diff --git a/Aspire.sln b/Aspire.sln
index fe5408bd274..1d86aaf65e5 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -1,4 +1,4 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
@@ -675,6 +675,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Azure.Npgsql.EntityF
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Components.Common.Tests", "tests\Aspire.Components.Common.Tests\Aspire.Components.Common.Tests.csproj", "{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.ContainerRegistry", "src\Aspire.Hosting.Azure.ContainerRegistry\Aspire.Hosting.Azure.ContainerRegistry.csproj", "{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -3961,6 +3965,18 @@ Global
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}.Release|x64.Build.0 = Release|Any CPU
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}.Release|x86.ActiveCfg = Release|Any CPU
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}.Release|x86.Build.0 = Release|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|x64.Build.0 = Debug|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|x86.Build.0 = Debug|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x64.ActiveCfg = Release|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x64.Build.0 = Release|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x86.ActiveCfg = Release|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -4285,6 +4301,7 @@ Global
{192747A2-9338-DECF-5C8C-28EB8E13829B} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{8FCA0CFA-7823-6A2F-342A-107A994915B0} = {C424395C-1235-41A4-BF55-07880A04368C}
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7} = {C424395C-1235-41A4-BF55-07880A04368C}
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {47DCFECF-5631-4BDE-A1EC-BE41E90F60C4}
diff --git a/playground/AzureContainerApps/AzureContainerApps.AppHost/api-roles-account-kv.module.bicep b/playground/AzureContainerApps/AzureContainerApps.AppHost/api-roles-account-kv.module.bicep
index b820675fb54..64c80f3913d 100644
--- a/playground/AzureContainerApps/AzureContainerApps.AppHost/api-roles-account-kv.module.bicep
+++ b/playground/AzureContainerApps/AzureContainerApps.AppHost/api-roles-account-kv.module.bicep
@@ -9,11 +9,11 @@ resource account_kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: account_kv_outputs_name
}
-resource account_kv_KeyVaultAdministrator 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(account_kv.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483'))
+resource account_kv_KeyVaultSecretsUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(account_kv.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6'))
properties: {
principalId: principalId
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483')
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')
principalType: 'ServicePrincipal'
}
scope: account_kv
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 e91cd7848a8..85c1808b635 100644
--- a/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj
+++ b/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj
@@ -19,6 +19,7 @@
+
diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs
index 9efff7d0211..50e0b2fc4a0 100644
--- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs
+++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs
@@ -98,12 +98,19 @@ public static IResourceBuilder AddAzureCon
infra.Add(identity);
- var containerRegistry = new ContainerRegistryService(Infrastructure.NormalizeBicepIdentifier($"{appEnvResource.Name}_acr"))
+ ContainerRegistryService? containerRegistry = null;
+ if (appEnvResource.TryGetLastAnnotation(out var registryReferenceAnnotation) && registryReferenceAnnotation.Registry is AzureProvisioningResource registry)
{
- Sku = new() { Name = ContainerRegistrySkuName.Basic },
- Tags = tags
- };
-
+ containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra);
+ }
+ else
+ {
+ containerRegistry = new ContainerRegistryService(Infrastructure.NormalizeBicepIdentifier($"{appEnvResource.Name}_acr"))
+ {
+ Sku = new() { Name = ContainerRegistrySkuName.Basic },
+ Tags = tags
+ };
+ }
infra.Add(containerRegistry);
var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, identity);
@@ -345,7 +352,7 @@ public static IResourceBuilder AddAzureCon
///
///
/// By default, the container app environment resources use a different naming convention than azd.
- ///
+ ///
/// This method allows for reusing the previously deployed resources if the application was deployed using
/// azd without calling
///
diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs
index 588f58fd06e..c452864c155 100644
--- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs
+++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs
@@ -1,3 +1,5 @@
+#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.
+
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
@@ -62,8 +64,8 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
#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.
r.Annotations.Add(new DeploymentTargetAnnotation(containerApp)
{
- ContainerRegistryInfo = caes.FirstOrDefault(),
- ComputeEnvironment = environment as IComputeEnvironmentResource // will be null if azd
+ ContainerRegistry = caes.FirstOrDefault(),
+ ComputeEnvironment = environment as IComputeEnvironmentResource
});
#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
}
diff --git a/src/Aspire.Hosting.Azure.ContainerRegistry/Aspire.Hosting.Azure.ContainerRegistry.csproj b/src/Aspire.Hosting.Azure.ContainerRegistry/Aspire.Hosting.Azure.ContainerRegistry.csproj
new file mode 100644
index 00000000000..8b2a84f7905
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.ContainerRegistry/Aspire.Hosting.Azure.ContainerRegistry.csproj
@@ -0,0 +1,23 @@
+
+
+
+ $(DefaultTargetFramework)
+ true
+ aspire integration hosting azure containerregistry
+ Azure Container Registry resource types for .NET Aspire.
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryExtensions.cs b/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryExtensions.cs
new file mode 100644
index 00000000000..a5eb8f5c0cc
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryExtensions.cs
@@ -0,0 +1,103 @@
+#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.
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Azure;
+using Aspire.Hosting.Azure.ContainerRegistry;
+using Azure.Provisioning;
+using Azure.Provisioning.ContainerRegistry;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Azure Container Registry resources to the application model.
+///
+public static class AzureContainerRegistryExtensions
+{
+ ///
+ /// Adds an Azure Container Registry resource to the application model.
+ ///
+ /// The builder for the distributed application.
+ /// The name of the resource.
+ /// A reference to the builder.
+ /// Thrown when is null.
+ /// Thrown when is null or empty.
+ public static IResourceBuilder AddAzureContainerRegistry(this IDistributedApplicationBuilder builder, [ResourceName] string name)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ builder.AddAzureProvisioning();
+
+ var configureInfrastructure = (AzureResourceInfrastructure infrastructure) =>
+ {
+ var registry = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infrastructure,
+ (identifier, name) =>
+ {
+ var resource = ContainerRegistryService.FromExisting(identifier);
+ resource.Name = name;
+ return resource;
+ },
+ (infrastructure) => new ContainerRegistryService(infrastructure.AspireResource.GetBicepIdentifier())
+ {
+ Sku = new() { Name = ContainerRegistrySkuName.Basic },
+ Tags = { { "aspire-resource-name", infrastructure.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);
+
+ // Don't add the resource to the infrastructure if we're in run mode.
+ if (builder.ExecutionContext.IsRunMode)
+ {
+ return builder.CreateResourceBuilder(resource);
+ }
+
+ return builder.AddResource(resource)
+ .WithAnnotation(new DefaultRoleAssignmentsAnnotation(new HashSet()));
+ }
+
+ ///
+ /// Configures a resource that implements to use the specified Azure Container Registry.
+ ///
+ /// The resource type that implements .
+ /// The resource builder for a resource that implements .
+ /// The resource builder for the to use.
+ /// A reference to the .
+ /// Thrown when or is null.
+ public static IResourceBuilder WithAzureContainerRegistry(this IResourceBuilder builder, IResourceBuilder registryBuilder)
+ where T : IResource, IComputeEnvironmentResource
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(registryBuilder);
+
+ // Add a ContainerRegistryReferenceAnnotation to indicate that the resource is using a specific registry
+ builder.WithAnnotation(new ContainerRegistryReferenceAnnotation(registryBuilder.Resource));
+
+ return builder;
+ }
+
+ ///
+ /// Adds role assignments to the specified Azure Container Registry resource.
+ ///
+ /// The type of the resource being configured.
+ /// The resource builder for the resource that will have role assignments.
+ /// The target Azure Container Registry resource.
+ /// The roles to assign to the resource.
+ /// A reference to the .
+ public static IResourceBuilder WithRoleAssignments(
+ this IResourceBuilder builder,
+ IResourceBuilder target,
+ params ContainerRegistryBuiltInRole[] roles)
+ where T : IResource
+ {
+ return builder.WithRoleAssignments(target, ContainerRegistryBuiltInRole.GetBuiltInRoleName, roles);
+ }
+}
diff --git a/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryResource.cs b/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryResource.cs
new file mode 100644
index 00000000000..07420d94052
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryResource.cs
@@ -0,0 +1,42 @@
+#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.
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Azure.Provisioning.ContainerRegistry;
+using Azure.Provisioning.Primitives;
+
+namespace Aspire.Hosting.Azure.ContainerRegistry;
+
+///
+/// Represents an Azure Container Registry resource.
+///
+public class AzureContainerRegistryResource(string name, Action configureInfrastructure)
+ : AzureProvisioningResource(name, configureInfrastructure), IContainerRegistry
+{
+ ///
+ /// The name of the Azure Container Registry.
+ ///
+ public BicepOutputReference RegistryName => new("name", this);
+
+ ///
+ /// The endpoint of the Azure Container Registry.
+ ///
+ public BicepOutputReference RegistryEndpoint => new("loginServer", this);
+
+ ///
+ ReferenceExpression IContainerRegistry.Name => ReferenceExpression.Create($"{RegistryName}");
+
+ ///
+ ReferenceExpression IContainerRegistry.Endpoint => ReferenceExpression.Create($"{RegistryEndpoint}");
+
+ ///
+ public override ProvisionableResource AddAsExistingResource(AzureResourceInfrastructure infra)
+ {
+ var store = ContainerRegistryService.FromExisting(this.GetBicepIdentifier());
+ store.Name = RegistryName.AsProvisioningParameter(infra);
+ infra.Add(store);
+ return store;
+ }
+}
diff --git a/src/Aspire.Hosting.Azure.ContainerRegistry/ContainerRegistryReferenceAnnotation.cs b/src/Aspire.Hosting.Azure.ContainerRegistry/ContainerRegistryReferenceAnnotation.cs
new file mode 100644
index 00000000000..e00152b9b62
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.ContainerRegistry/ContainerRegistryReferenceAnnotation.cs
@@ -0,0 +1,23 @@
+#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.
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.Azure;
+
+///
+/// Annotation that indicates a resource is using a specific container registry.
+///
+///
+/// Initializes a new instance of the class.
+///
+/// The container registry resource.
+public class ContainerRegistryReferenceAnnotation(IContainerRegistry registry) : IResourceAnnotation
+{
+ ///
+ /// Gets the container registry resource.
+ ///
+ public IContainerRegistry Registry { get; } = registry;
+}
diff --git a/src/Aspire.Hosting.Azure/AzurePublishingContext.cs b/src/Aspire.Hosting.Azure/AzurePublishingContext.cs
index 4114b545999..82080304073 100644
--- a/src/Aspire.Hosting.Azure/AzurePublishingContext.cs
+++ b/src/Aspire.Hosting.Azure/AzurePublishingContext.cs
@@ -238,10 +238,10 @@ void CaptureBicepOutputs(object value)
File.Copy(file.Path, modulePath, true);
// Capture any bicep outputs from the registry info as it may be needed
- Visit(annotation.ContainerRegistryInfo?.Name, CaptureBicepOutputs);
- Visit(annotation.ContainerRegistryInfo?.Endpoint, CaptureBicepOutputs);
+ Visit(annotation.ContainerRegistry?.Name, CaptureBicepOutputs);
+ Visit(annotation.ContainerRegistry?.Endpoint, CaptureBicepOutputs);
- if (annotation.ContainerRegistryInfo is IAzureContainerRegistry acr)
+ if (annotation.ContainerRegistry is IAzureContainerRegistry acr)
{
Visit(acr.ManagedIdentityId, CaptureBicepOutputs);
}
diff --git a/src/Aspire.Hosting.Docker/DockerComposeInfrastructure.cs b/src/Aspire.Hosting.Docker/DockerComposeInfrastructure.cs
index 1e937c36a51..05d19d16198 100644
--- a/src/Aspire.Hosting.Docker/DockerComposeInfrastructure.cs
+++ b/src/Aspire.Hosting.Docker/DockerComposeInfrastructure.cs
@@ -59,7 +59,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
#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.
r.Annotations.Add(new DeploymentTargetAnnotation(serviceResource)
{
- ComputeEnvironment = environment,
+ ComputeEnvironment = environment
});
#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
}
diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesInfrastructure.cs b/src/Aspire.Hosting.Kubernetes/KubernetesInfrastructure.cs
index 98f0523057e..e1d418916f7 100644
--- a/src/Aspire.Hosting.Kubernetes/KubernetesInfrastructure.cs
+++ b/src/Aspire.Hosting.Kubernetes/KubernetesInfrastructure.cs
@@ -59,7 +59,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
#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.
r.Annotations.Add(new DeploymentTargetAnnotation(serviceResource)
{
- ComputeEnvironment = environment,
+ ComputeEnvironment = environment
});
#pragma warning restore ASPIRECOMPUTE001
}
diff --git a/src/Aspire.Hosting/ApplicationModel/ContainerResource.cs b/src/Aspire.Hosting/ApplicationModel/ContainerResource.cs
index f546b0b1c30..a3c03df7f4a 100644
--- a/src/Aspire.Hosting/ApplicationModel/ContainerResource.cs
+++ b/src/Aspire.Hosting/ApplicationModel/ContainerResource.cs
@@ -1,3 +1,5 @@
+#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.
+
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
@@ -10,9 +12,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// An optional container entrypoint.
public class ContainerResource(string name, string? entrypoint = null)
: Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithEndpoints, IResourceWithWaitSupport,
-#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.
IComputeResource
-#pragma warning restore ASPIRECOMPUTE001
{
///
/// The container Entrypoint.
diff --git a/src/Aspire.Hosting/ApplicationModel/DeploymentTargetAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/DeploymentTargetAnnotation.cs
index 83878029af9..ecd130e1098 100644
--- a/src/Aspire.Hosting/ApplicationModel/DeploymentTargetAnnotation.cs
+++ b/src/Aspire.Hosting/ApplicationModel/DeploymentTargetAnnotation.cs
@@ -20,7 +20,7 @@ public sealed class DeploymentTargetAnnotation(IResource target) : IResourceAnno
/// the deployment target, if the deployment target is an image-based environment.
///
[Experimental("ASPIRECOMPUTE001")]
- public IContainerRegistry? ContainerRegistryInfo { get; set; }
+ public IContainerRegistry? ContainerRegistry { get; set; }
///
/// Gets or sets the compute environment resource associated with the deployment target.
diff --git a/src/Aspire.Hosting/ApplicationModel/ExecutableResource.cs b/src/Aspire.Hosting/ApplicationModel/ExecutableResource.cs
index 3b25c775762..560686db5fd 100644
--- a/src/Aspire.Hosting/ApplicationModel/ExecutableResource.cs
+++ b/src/Aspire.Hosting/ApplicationModel/ExecutableResource.cs
@@ -1,3 +1,5 @@
+#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.
+
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
@@ -14,9 +16,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// The working directory of the executable. Can be empty.
public class ExecutableResource(string name, string command, string workingDirectory)
: Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithEndpoints, IResourceWithWaitSupport,
-#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.
IComputeResource
-#pragma warning restore ASPIRECOMPUTE001
{
///
/// Gets the command associated with this executable resource.
diff --git a/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs b/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs
index a44eb93c317..ea7a7302d86 100644
--- a/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs
+++ b/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs
@@ -1,3 +1,5 @@
+#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.
+
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
@@ -9,9 +11,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// The name of the resource.
public class ProjectResource(string name)
: Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithServiceDiscovery, IResourceWithWaitSupport,
-#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.
IComputeResource
-#pragma warning restore ASPIRECOMPUTE001
{
// Keep track of the config host for each Kestrel endpoint annotation
internal Dictionary KestrelEndpointAnnotationHosts { get; } = new();
@@ -34,7 +34,7 @@ internal bool ShouldInjectEndpointEnvironment(EndpointReference e)
// If any filter rejects the endpoint, skip it
return !Annotations.OfType()
- .Select(a => a.Filter)
- .Any(f => !f(endpoint));
+ .Select(a => a.Filter)
+ .Any(f => !f(endpoint));
}
}
diff --git a/src/Aspire.Hosting/DistributedApplication.cs b/src/Aspire.Hosting/DistributedApplication.cs
index 68985c74ad7..c72eaad79ad 100644
--- a/src/Aspire.Hosting/DistributedApplication.cs
+++ b/src/Aspire.Hosting/DistributedApplication.cs
@@ -411,7 +411,7 @@ public virtual async Task RunAsync(CancellationToken cancellationToken = default
{
await ExecuteBeforeStartHooksAsync(cancellationToken).ConfigureAwait(false);
}
-
+
await _host.RunAsync(cancellationToken).ConfigureAwait(false);
}
diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs
index a8524401996..5615e5c795f 100644
--- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs
@@ -1923,7 +1923,7 @@ public static IResourceBuilder WithParentRelationship(
///
/// var builder = DistributedApplication.CreateBuilder(args);
/// var backend = builder.AddProject<Projects.Backend>("backend");
- ///
+ ///
/// var frontend = builder.AddProject<Projects.Manager>("frontend")
/// .WithParentRelationship(backend.Resource);
///
diff --git a/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj b/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj
index 291bd8eca75..e3b52ef4a38 100644
--- a/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj
+++ b/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj
@@ -24,6 +24,7 @@
+
diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs
index dddcb7dae61..6d23ab7affc 100644
--- a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs
@@ -7,6 +7,7 @@
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure.AppContainers;
+using Aspire.Hosting.Azure.ContainerRegistry;
using Aspire.Hosting.Utils;
using Azure.Provisioning;
using Azure.Provisioning.AppContainers;
@@ -68,11 +69,11 @@ public async Task AddContainerAppsInfrastructureAddsDeploymentTargetWithContaine
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param outputs_azure_container_apps_environment_default_domain string
param outputs_azure_container_apps_environment_id string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -152,17 +153,17 @@ public async Task AddDockerfileWithAppsInfrastructureAddsDeploymentTargetWithCon
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
param env_outputs_azure_container_registry_endpoint string
-
+
param env_outputs_azure_container_registry_managed_identity_id string
-
+
param api_containerimage string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -217,11 +218,12 @@ public async Task AddContainerAppEnvironmentAddsDeploymentTargetWithContainerApp
var model = app.Services.GetRequiredService();
- var container = Assert.Single(model.GetProjectResources());
+ var container = Assert.IsType(Assert.Single(model.GetProjectResources()), exactMatch: false);
var target = container.GetDeploymentTargetAnnotation();
- Assert.Same(env.Resource, target?.ComputeEnvironment);
+ Assert.NotNull(target);
+ Assert.Same(env.Resource, target.ComputeEnvironment);
var resource = target?.DeploymentTarget as AzureProvisioningResource;
Assert.NotNull(resource);
@@ -356,13 +358,14 @@ public async Task AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastruc
var model = app.Services.GetRequiredService();
- var container = Assert.Single(model.GetContainerResources());
+ var container = Assert.IsType(Assert.Single(model.GetContainerResources()), exactMatch: false);
var target = container.GetDeploymentTargetAnnotation();
- Assert.Same(infra.Resource, target?.ComputeEnvironment);
+ Assert.NotNull(target);
+ Assert.Same(infra.Resource, target.ComputeEnvironment);
- var resource = target?.DeploymentTarget as AzureProvisioningResource;
+ var resource = target.DeploymentTarget as AzureProvisioningResource;
Assert.NotNull(resource);
var (manifest, bicep) = await GetManifestWithBicep(resource);
@@ -391,19 +394,19 @@ public async Task AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastruc
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param infra_outputs_azure_container_apps_environment_default_domain string
param infra_outputs_azure_container_apps_environment_id string
-
+
param infra_outputs_azure_container_registry_endpoint string
-
+
param infra_outputs_azure_container_registry_managed_identity_id string
-
+
param api_containerimage string
-
+
param env string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -464,11 +467,12 @@ public async Task CanTweakContainerAppEnvironmentUsingPublishAsContainerAppOnExe
var model = app.Services.GetRequiredService();
- var container = Assert.Single(model.GetContainerResources());
+ var container = Assert.IsType(Assert.Single(model.GetContainerResources()), exactMatch: false);
var target = container.GetDeploymentTargetAnnotation();
- Assert.Same(env.Resource, target?.ComputeEnvironment);
+ Assert.NotNull(target);
+ Assert.Same(env.Resource, target.ComputeEnvironment);
var resource = target?.DeploymentTarget as AzureProvisioningResource;
Assert.NotNull(resource);
@@ -498,17 +502,17 @@ public async Task CanTweakContainerAppEnvironmentUsingPublishAsContainerAppOnExe
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
param env_outputs_azure_container_registry_endpoint string
-
+
param env_outputs_azure_container_registry_managed_identity_id string
-
+
param api_containerimage string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -607,15 +611,15 @@ public async Task AddContainerAppsInfrastructureWithParameterReference()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
param value string
-
+
param minReplicas string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -679,11 +683,11 @@ public async Task AddContainerAppsEntrypointAndArgs()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -831,46 +835,46 @@ public async Task ProjectWithManyReferenceTypes()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param api_identity_outputs_id string
-
+
param api_identity_outputs_clientid string
-
+
param api_containerport string
-
+
param mydb_outputs_connectionstring string
-
+
param storage_outputs_blobendpoint string
-
+
param pg_kv_outputs_name string
-
+
@secure()
param value0_value string
-
+
param value1_value string
-
+
@secure()
param cs_connectionstring string
-
+
param env_outputs_azure_container_apps_environment_default_domain string
-
+
param env_outputs_azure_container_apps_environment_id string
-
+
param env_outputs_azure_container_registry_endpoint string
-
+
param env_outputs_azure_container_registry_managed_identity_id string
-
+
param api_containerimage string
-
+
resource pg_kv_outputs_name_kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: pg_kv_outputs_name
}
-
+
resource pg_kv_outputs_name_kv_connectionstrings__db 'Microsoft.KeyVault/vaults/secrets@2023-07-01' existing = {
name: 'connectionstrings--db'
parent: pg_kv_outputs_name_kv
}
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -1044,9 +1048,9 @@ param api_containerimage string
}
output id string = api_identity.id
-
+
output clientId string = api_identity.properties.clientId
-
+
output principalId string = api_identity.properties.principalId
output principalName string = api_identity.name
@@ -1243,11 +1247,11 @@ public async Task PublishAsContainerAppInfluencesContainerAppDefinition()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -1328,15 +1332,15 @@ public async Task ConfigureCustomDomainMutatesIngress()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
param certificateName string
-
+
param customDomain string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -1432,17 +1436,17 @@ public async Task ConfigureDuplicateCustomDomainMutatesIngress()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
param initialCertificateName string
-
+
param customDomain string
-
+
param expectedCertificateName string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -1541,19 +1545,19 @@ public async Task ConfigureMultipleCustomDomainsMutatesIngress()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
param certificateName1 string
-
+
param customDomain1 string
-
+
param certificateName2 string
-
+
param customDomain2 string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -1790,43 +1794,43 @@ public async Task SecretOutputHandling()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param api_identity_outputs_id string
-
+
param api_identity_outputs_clientid string
-
+
param mydb_kv_outputs_name string
-
+
param mydb_secretoutputs string
-
+
@secure()
param mydb_secretoutputs_connectionstring string
-
+
@secure()
param mydb_secretoutputs_connectionstring1 string
-
+
param outputs_azure_container_apps_environment_default_domain string
param outputs_azure_container_apps_environment_id string
-
+
resource mydb_kv_outputs_name_kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: mydb_kv_outputs_name
}
-
+
resource mydb_secretoutputs_kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: mydb_secretoutputs
}
-
+
resource mydb_kv_outputs_name_kv_connectionstrings__mydb 'Microsoft.KeyVault/vaults/secrets@2023-07-01' existing = {
name: 'connectionstrings--mydb'
parent: mydb_kv_outputs_name_kv
}
-
+
resource mydb_secretoutputs_kv_connectionString 'Microsoft.KeyVault/vaults/secrets@2023-07-01' existing = {
name: 'connectionString'
parent: mydb_secretoutputs_kv
}
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -1983,11 +1987,11 @@ public async Task CanCustomizeWithProvisioningBuildOptions()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
resource api1 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api1-my'
location: location
@@ -2074,11 +2078,11 @@ public async Task ExternalEndpointBecomesIngress()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -2157,11 +2161,11 @@ public async Task FirstHttpEndpointBecomesIngress()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -2247,11 +2251,11 @@ public async Task EndpointWithHttp2SetsTransportToH2()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -2334,19 +2338,19 @@ public async Task ProjectUsesTheTargetPortAsADefaultPortForFirstHttpEndpoint()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param api_containerport string
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
param env_outputs_azure_container_registry_endpoint string
-
+
param env_outputs_azure_container_registry_managed_identity_id string
-
+
param api_containerimage string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -2502,21 +2506,21 @@ public async Task RoleAssignmentsWithAsExisting()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param api_identity_outputs_id string
-
+
param api_identity_outputs_clientid string
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
param env_outputs_azure_container_registry_endpoint string
-
+
param env_outputs_azure_container_registry_managed_identity_id string
-
+
param api_containerimage string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -2707,23 +2711,23 @@ public async Task RoleAssignmentsWithAsExistingCosmosDB()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param api_identity_outputs_id string
-
+
param api_identity_outputs_clientid string
-
+
param cosmos_outputs_connectionstring string
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
param env_outputs_azure_container_registry_endpoint string
-
+
param env_outputs_azure_container_registry_managed_identity_id string
-
+
param api_containerimage string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -2921,23 +2925,23 @@ public async Task RoleAssignmentsWithAsExistingRedis()
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-
+
param api_identity_outputs_id string
-
+
param api_identity_outputs_clientid string
-
+
param redis_outputs_connectionstring string
-
+
param env_outputs_azure_container_apps_environment_default_domain string
param env_outputs_azure_container_apps_environment_id string
-
+
param env_outputs_azure_container_registry_endpoint string
-
+
param env_outputs_azure_container_registry_managed_identity_id string
-
+
param api_containerimage string
-
+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
@@ -3662,6 +3666,299 @@ param principalName string
Assert.Equal(expectedBicep, bicep);
}
+ [Fact]
+ public async Task ContainerAppEnvironmentWithCustomRegistry()
+ {
+ var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
+
+ // Create a custom registry
+ var registry = builder.AddAzureContainerRegistry("customregistry");
+
+ // Create a container app environment and associate it with the custom registry
+ builder.AddAzureContainerAppEnvironment("env")
+ .WithAzureContainerRegistry(registry);
+
+ // Add a container that will use the environment
+ builder.AddProject("api", launchProfileName: null)
+ .WithHttpEndpoint();
+
+ using var app = builder.Build();
+
+ await ExecuteBeforeStartHooksAsync(app, default);
+
+ var model = app.Services.GetRequiredService();
+
+ // Verify environment resource exists
+ var environment = Assert.Single(model.Resources.OfType());
+
+ // Verify project resource exists
+ var project = Assert.Single(model.GetProjectResources());
+
+ // Get the bicep for the environment
+ var (envManifest, envBicep) = await GetManifestWithBicep(environment);
+
+ // Verify the environment manifest
+ var expectedEnvManifest =
+ """
+ {
+ "type": "azure.bicep.v0",
+ "path": "env.module.bicep",
+ "params": {
+ "customregistry_outputs_name": "{customregistry.outputs.name}",
+ "userPrincipalId": ""
+ }
+ }
+ """;
+
+ Assert.Equal(expectedEnvManifest, envManifest.ToString());
+
+ // Use existing reference for registry, assign ACR pull role to the environment's managed identity
+ // and set the registry as the container's registry via the output references
+ var expectedEnvBicep =
+ """
+ @description('The location for the resource(s) to be deployed.')
+ param location string = resourceGroup().location
+
+ param userPrincipalId string
+
+ param tags object = { }
+
+ param customregistry_outputs_name string
+
+ resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
+ name: take('env_mi-${uniqueString(resourceGroup().id)}', 128)
+ location: location
+ tags: tags
+ }
+
+ resource customregistry 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = {
+ name: customregistry_outputs_name
+ }
+
+ resource customregistry_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(customregistry.id, env_mi.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d'))
+ properties: {
+ principalId: env_mi.properties.principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
+ principalType: 'ServicePrincipal'
+ }
+ scope: customregistry
+ }
+
+ resource env_law 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
+ name: take('envlaw-${uniqueString(resourceGroup().id)}', 63)
+ location: location
+ properties: {
+ sku: {
+ name: 'PerGB2018'
+ }
+ }
+ tags: tags
+ }
+
+ resource env 'Microsoft.App/managedEnvironments@2024-03-01' = {
+ name: take('env${uniqueString(resourceGroup().id)}', 24)
+ location: location
+ properties: {
+ appLogsConfiguration: {
+ destination: 'log-analytics'
+ logAnalyticsConfiguration: {
+ customerId: env_law.properties.customerId
+ sharedKey: env_law.listKeys().primarySharedKey
+ }
+ }
+ workloadProfiles: [
+ {
+ name: 'consumption'
+ workloadProfileType: 'Consumption'
+ }
+ ]
+ }
+ tags: tags
+ }
+
+ resource aspireDashboard 'Microsoft.App/managedEnvironments/dotNetComponents@2024-10-02-preview' = {
+ name: 'aspire-dashboard'
+ properties: {
+ componentType: 'AspireDashboard'
+ }
+ parent: env
+ }
+
+ resource env_Contributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(env.id, userPrincipalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c'))
+ properties: {
+ principalId: userPrincipalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
+ }
+ scope: env
+ }
+
+ output MANAGED_IDENTITY_NAME string = env_mi.name
+
+ output MANAGED_IDENTITY_PRINCIPAL_ID string = env_mi.properties.principalId
+
+ 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 = customregistry_outputs_name
+
+ output AZURE_CONTAINER_REGISTRY_ENDPOINT string = customregistry.properties.loginServer
+
+ output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = env_mi.id
+
+ output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = env.name
+
+ output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = env.id
+
+ output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = env.properties.defaultDomain
+ """;
+ Assert.Equal(expectedEnvBicep, envBicep);
+
+ // Verify container has correct deployment target
+ project.TryGetLastAnnotation(out var target);
+ var projectResource = target?.DeploymentTarget as AzureProvisioningResource;
+ Assert.NotNull(projectResource);
+
+ // Get the bicep for the container
+ var (containerManifest, containerBicep) = await GetManifestWithBicep(projectResource);
+
+ // Verify container manifest references the environment outputs
+ var expectedContainerManifest =
+ """
+ {
+ "type": "azure.bicep.v0",
+ "path": "api.module.bicep",
+ "params": {
+ "api_containerport": "{api.containerPort}",
+ "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}",
+ "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}",
+ "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}",
+ "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}",
+ "api_containerimage": "{api.containerImage}"
+ }
+ }
+ """;
+
+ Assert.Equal(expectedContainerManifest, containerManifest.ToString());
+
+ var expectedContainerBicep =
+ """
+ @description('The location for the resource(s) to be deployed.')
+ param location string = resourceGroup().location
+
+ param api_containerport string
+
+ param env_outputs_azure_container_apps_environment_default_domain string
+
+ param env_outputs_azure_container_apps_environment_id string
+
+ param env_outputs_azure_container_registry_endpoint string
+
+ param env_outputs_azure_container_registry_managed_identity_id string
+
+ param api_containerimage string
+
+ resource api 'Microsoft.App/containerApps@2024-03-01' = {
+ name: 'api'
+ location: location
+ properties: {
+ configuration: {
+ activeRevisionsMode: 'Single'
+ ingress: {
+ external: false
+ targetPort: api_containerport
+ transport: 'http'
+ }
+ registries: [
+ {
+ server: env_outputs_azure_container_registry_endpoint
+ identity: env_outputs_azure_container_registry_managed_identity_id
+ }
+ ]
+ }
+ environmentId: env_outputs_azure_container_apps_environment_id
+ template: {
+ containers: [
+ {
+ image: api_containerimage
+ name: 'api'
+ env: [
+ {
+ name: 'OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES'
+ value: 'true'
+ }
+ {
+ name: 'OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES'
+ value: 'true'
+ }
+ {
+ name: 'OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY'
+ value: 'in_memory'
+ }
+ {
+ name: 'ASPNETCORE_FORWARDEDHEADERS_ENABLED'
+ value: 'true'
+ }
+ {
+ name: 'HTTP_PORTS'
+ value: api_containerport
+ }
+ ]
+ }
+ ]
+ scale: {
+ minReplicas: 1
+ }
+ }
+ }
+ identity: {
+ type: 'UserAssigned'
+ userAssignedIdentities: {
+ '${env_outputs_azure_container_registry_managed_identity_id}': { }
+ }
+ }
+ }
+ """;
+ Assert.Equal(expectedContainerBicep, containerBicep);
+
+ // Verify the Azure Container Registry resource manifest and bicep
+ var containerRegistry = Assert.Single(model.Resources.OfType());
+ var (registryManifest, registryBicep) = await GetManifestWithBicep(containerRegistry);
+
+ var expectedRegistryManifest =
+ """
+ {
+ "type": "azure.bicep.v0",
+ "path": "customregistry.module.bicep"
+ }
+ """;
+ Assert.Equal(expectedRegistryManifest, registryManifest.ToString());
+
+ var expectedRegistryBicep =
+ """
+ @description('The location for the resource(s) to be deployed.')
+ param location string = resourceGroup().location
+
+ resource customregistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
+ name: take('customregistry${uniqueString(resourceGroup().id)}', 50)
+ location: location
+ sku: {
+ name: 'Basic'
+ }
+ tags: {
+ 'aspire-resource-name': 'customregistry'
+ }
+ }
+
+ output name string = customregistry.name
+
+ output loginServer string = customregistry.properties.loginServer
+ """;
+ Assert.Equal(expectedRegistryBicep, registryBicep);
+ }
+
private static Task<(JsonNode ManifestNode, string BicepText)> GetManifestWithBicep(IResource resource) =>
AzureManifestUtils.GetManifestWithBicep(resource, skipPreparer: true);
diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureContainerRegistryTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureContainerRegistryTests.cs
new file mode 100644
index 00000000000..54d80208437
--- /dev/null
+++ b/tests/Aspire.Hosting.Azure.Tests/AzureContainerRegistryTests.cs
@@ -0,0 +1,179 @@
+#pragma warning disable ASPIRECOMPUTE001
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Azure.AppContainers;
+using Aspire.Hosting.Azure.ContainerRegistry;
+using Aspire.Hosting.Utils;
+using Azure.Provisioning.ContainerRegistry;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+using static Aspire.Hosting.Utils.AzureManifestUtils;
+
+namespace Aspire.Hosting.Azure.Tests;
+
+public class AzureContainerRegistryTests(ITestOutputHelper output)
+{
+ [Fact]
+ public async Task AddAzureContainerRegistry_AddsResourceAndImplementsIContainerRegistry()
+ {
+ var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
+
+ _ = builder.AddAzureContainerRegistry("acr");
+
+ // Build & execute hooks
+ using var app = builder.Build();
+ await ExecuteBeforeStartHooksAsync(app, default);
+
+ var model = app.Services.GetRequiredService();
+
+ var registryResource = Assert.Single(model.Resources.OfType());
+ var registryInterface = Assert.IsType(registryResource, exactMatch: false);
+
+ Assert.NotNull(registryInterface);
+ Assert.NotNull(registryInterface.Name);
+ Assert.NotNull(registryInterface.Endpoint);
+ }
+
+ [Fact]
+ public async Task WithRegistry_AttachesContainerRegistryReferenceAnnotation()
+ {
+ var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
+
+ var registryBuilder = builder.AddAzureContainerRegistry("acr");
+ _ = builder.AddAzureContainerAppEnvironment("env")
+ .WithAzureContainerRegistry(registryBuilder); // Extension method under test
+
+ using var app = builder.Build();
+ await ExecuteBeforeStartHooksAsync(app, default);
+
+ var model = app.Services.GetRequiredService();
+ var environment = Assert.Single(model.Resources.OfType());
+
+ Assert.True(environment.TryGetLastAnnotation(out var annotation));
+ Assert.Same(registryBuilder.Resource, annotation!.Registry);
+ }
+
+ [Fact]
+ public async Task AddAzureContainerRegistry_GeneratesCorrectManifestAndBicep()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+
+ var acr = builder.AddAzureContainerRegistry("acr");
+
+ var manifest = await GetManifestWithBicep(acr.Resource);
+
+ var expectedManifest = """
+ {
+ "type": "azure.bicep.v0",
+ "path": "acr.module.bicep"
+ }
+ """;
+
+ output.WriteLine(manifest.ManifestNode.ToString());
+ Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
+
+ var expectedBicep = """
+ @description('The location for the resource(s) to be deployed.')
+ param location string = resourceGroup().location
+
+ resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
+ name: take('acr${uniqueString(resourceGroup().id)}', 50)
+ location: location
+ sku: {
+ name: 'Basic'
+ }
+ tags: {
+ 'aspire-resource-name': 'acr'
+ }
+ }
+
+ output name string = acr.name
+
+ output loginServer string = acr.properties.loginServer
+ """;
+
+ output.WriteLine(manifest.BicepText);
+ Assert.Equal(expectedBicep, manifest.BicepText);
+ }
+
+ [Fact]
+ public async Task WithRoleAssignments_GeneratesCorrectRoleAssignmentBicep()
+ {
+ var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
+
+ // Add container app environment since it's required for role assignments
+ builder.AddAzureContainerAppEnvironment("env");
+
+ // Create a container registry and assign roles to a project
+ var acr = builder.AddAzureContainerRegistry("acr");
+ builder.AddProject("api", launchProfileName: null)
+ .WithRoleAssignments(acr, ContainerRegistryBuiltInRole.AcrPull, ContainerRegistryBuiltInRole.AcrPush);
+
+ using var app = builder.Build();
+ await ExecuteBeforeStartHooksAsync(app, default);
+
+ var model = app.Services.GetRequiredService();
+ var rolesResource = Assert.Single(model.Resources.OfType(), r => r.Name == "api-roles-acr");
+
+ var (rolesManifest, rolesBicep) = await GetManifestWithBicep(rolesResource);
+
+ var expectedRolesManifest =
+ """
+ {
+ "type": "azure.bicep.v0",
+ "path": "api-roles-acr.module.bicep",
+ "params": {
+ "acr_outputs_name": "{acr.outputs.name}",
+ "principalId": "{api-identity.outputs.principalId}"
+ }
+ }
+ """;
+
+ Assert.Equal(expectedRolesManifest, rolesManifest.ToString());
+
+ var expectedRolesBicep =
+ """
+ @description('The location for the resource(s) to be deployed.')
+ param location string = resourceGroup().location
+
+ param acr_outputs_name string
+
+ param principalId string
+
+ resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = {
+ name: acr_outputs_name
+ }
+
+ resource acr_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(acr.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
+ principalType: 'ServicePrincipal'
+ }
+ scope: acr
+ }
+
+ resource acr_AcrPush 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(acr.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec')
+ principalType: 'ServicePrincipal'
+ }
+ scope: acr
+ }
+ """;
+
+ output.WriteLine(rolesBicep);
+ Assert.Equal(expectedRolesBicep, rolesBicep);
+ }
+
+ private sealed class Project : IProjectMetadata
+ {
+ public string ProjectPath => "project";
+ }
+}
diff --git a/tests/Aspire.Hosting.Azure.Tests/ContainerRegistryTests.cs b/tests/Aspire.Hosting.Azure.Tests/ContainerRegistryTests.cs
index 4d2c0de0250..1befec70787 100644
--- a/tests/Aspire.Hosting.Azure.Tests/ContainerRegistryTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/ContainerRegistryTests.cs
@@ -64,10 +64,10 @@ public async Task ContainerRegistryInfoFlowsToDeploymentTargetForProjects()
Assert.NotNull(target);
// Verify that ContainerRegistryInfo property is not null for project resources
- Assert.NotNull(target.ContainerRegistryInfo);
+ Assert.NotNull(target.ContainerRegistry);
// Verify that ContainerRegistryInfo is of type IContainerRegistry
- var registry = Assert.IsType(target.ContainerRegistryInfo, exactMatch: false);
+ var registry = Assert.IsType(target.ContainerRegistry, exactMatch: false);
// Verify registry properties are available
Assert.NotNull(registry.Name);
@@ -157,10 +157,10 @@ public Task PublishAsync(DistributedApplicationModel model, CancellationToken ca
foreach (var resource in model.Resources)
{
if (resource.TryGetLastAnnotation(out var annotation) &&
- annotation.ContainerRegistryInfo != null)
+ annotation.ContainerRegistry != null)
{
ComputeResourceRegistryFound = true;
- ComputeResourceRegistry = annotation.ContainerRegistryInfo;
+ ComputeResourceRegistry = annotation.ContainerRegistry;
if (ComputeResourceRegistry is IAzureContainerRegistry azureRegistry)
{
AzureContainerRegistry = azureRegistry;
diff --git a/tests/Aspire.Hosting.Azure.Tests/ExistingAzureResourceTests.cs b/tests/Aspire.Hosting.Azure.Tests/ExistingAzureResourceTests.cs
index 70a48fa0516..373a89f664c 100644
--- a/tests/Aspire.Hosting.Azure.Tests/ExistingAzureResourceTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/ExistingAzureResourceTests.cs
@@ -1391,4 +1391,92 @@ param keyVaultName string
output.WriteLine(BicepText);
Assert.Equal(expectedBicep, BicepText);
}
+
+ [Fact]
+ public async Task SupportsExistingAzureContainerRegistryInRunMode()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
+
+ var existingResourceName = builder.AddParameter("existingResourceName");
+ var acr = builder.AddAzureContainerRegistry("acr")
+ .RunAsExisting(existingResourceName, resourceGroupParameter: default);
+
+ var (ManifestNode, BicepText) = await AzureManifestUtils.GetManifestWithBicep(acr.Resource);
+
+ var expectedManifest = """
+ {
+ "type": "azure.bicep.v0",
+ "path": "acr.module.bicep",
+ "params": {
+ "existingResourceName": "{existingResourceName.value}"
+ }
+ }
+ """;
+
+ Assert.Equal(expectedManifest, ManifestNode.ToString());
+
+ var expectedBicep = """
+ @description('The location for the resource(s) to be deployed.')
+ param location string = resourceGroup().location
+
+ param existingResourceName string
+
+ resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = {
+ name: existingResourceName
+ }
+
+ output name string = existingResourceName
+
+ output loginServer string = acr.properties.loginServer
+ """;
+
+ output.WriteLine(BicepText);
+ Assert.Equal(expectedBicep, BicepText);
+ }
+
+ [Fact]
+ public async Task SupportsExistingAzureContainerRegistryInPublishMode()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
+
+ var existingResourceName = builder.AddParameter("existingResourceName");
+ var existingResourceGroupName = builder.AddParameter("existingResourceGroupName");
+ var acr = builder.AddAzureContainerRegistry("acr")
+ .PublishAsExisting(existingResourceName, existingResourceGroupName);
+
+ var (ManifestNode, BicepText) = await AzureManifestUtils.GetManifestWithBicep(acr.Resource);
+
+ var expectedManifest = """
+ {
+ "type": "azure.bicep.v1",
+ "path": "acr.module.bicep",
+ "params": {
+ "existingResourceName": "{existingResourceName.value}"
+ },
+ "scope": {
+ "resourceGroup": "{existingResourceGroupName.value}"
+ }
+ }
+ """;
+
+ Assert.Equal(expectedManifest, ManifestNode.ToString());
+
+ var expectedBicep = """
+ @description('The location for the resource(s) to be deployed.')
+ param location string = resourceGroup().location
+
+ param existingResourceName string
+
+ resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = {
+ name: existingResourceName
+ }
+
+ output name string = existingResourceName
+
+ output loginServer string = acr.properties.loginServer
+ """;
+
+ output.WriteLine(BicepText);
+ Assert.Equal(expectedBicep, BicepText);
+ }
}
diff --git a/tests/Aspire.Hosting.Docker.Tests/DockerComposeTests.cs b/tests/Aspire.Hosting.Docker.Tests/DockerComposeTests.cs
index 544e678078d..283ab203a3e 100644
--- a/tests/Aspire.Hosting.Docker.Tests/DockerComposeTests.cs
+++ b/tests/Aspire.Hosting.Docker.Tests/DockerComposeTests.cs
@@ -31,4 +31,4 @@ public async Task DockerComposeSetsComputeEnvironment()
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);
-}
\ No newline at end of file
+}
diff --git a/tests/Shared/RepoTesting/Directory.Packages.Helix.props b/tests/Shared/RepoTesting/Directory.Packages.Helix.props
index 3a74f01ca7e..09b26eff193 100644
--- a/tests/Shared/RepoTesting/Directory.Packages.Helix.props
+++ b/tests/Shared/RepoTesting/Directory.Packages.Helix.props
@@ -22,6 +22,7 @@
+