Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
Allow customizing Azure ContainerApp resources with ProvisioningBuild…
…Options.

Fix #6496
  • Loading branch information
eerhardt committed Oct 25, 2024
commit b1760c1e0dfff2fb9757e7b9ee39af8b9350bb72
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@
using Azure.Provisioning.KeyVault;
using Azure.Provisioning.Resources;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Aspire.Hosting.Azure;

/// <summary>
/// Represents the infrastructure for Azure Container Apps within the Aspire Hosting environment.
/// Implements the <see cref="IDistributedApplicationLifecycleHook"/> interface to provide lifecycle hooks for distributed applications.
/// </summary>
internal sealed class AzureContainerAppsInfrastructure(ILogger<AzureContainerAppsInfrastructure> logger, DistributedApplicationExecutionContext executionContext) : IDistributedApplicationLifecycleHook
internal sealed class AzureContainerAppsInfrastructure(
ILogger<AzureContainerAppsInfrastructure> logger,
IOptions<AzureProvisioningOptions> provisioningOptions,
DistributedApplicationExecutionContext executionContext) : IDistributedApplicationLifecycleHook
{
public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -48,7 +52,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
continue;
}

var containerApp = await containerAppEnvironmentContext.CreateContainerAppAsync(r, executionContext, cancellationToken).ConfigureAwait(false);
var containerApp = await containerAppEnvironmentContext.CreateContainerAppAsync(r, provisioningOptions.Value, executionContext, cancellationToken).ConfigureAwait(false);

r.Annotations.Add(new DeploymentTargetAnnotation(containerApp));
}
Expand All @@ -74,11 +78,12 @@ IManifestExpressionProvider clientId

private readonly Dictionary<IResource, ContainerAppContext> _containerApps = [];

public async Task<AzureBicepResource> CreateContainerAppAsync(IResource resource, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken)
public async Task<AzureBicepResource> CreateContainerAppAsync(IResource resource, AzureProvisioningOptions provisioningOptions, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken)
{
var context = await ProcessResourceAsync(resource, executionContext, cancellationToken).ConfigureAwait(false);

var provisioningResource = new AzureProvisioningResource(resource.Name, context.BuildContainerApp);
provisioningResource.ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions;

provisioningResource.Annotations.Add(new ManifestPublishingCallbackAnnotation(provisioningResource.WriteToManifest));

Expand Down
90 changes: 90 additions & 0 deletions tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Azure.Provisioning;
using Azure.Provisioning.AppContainers;
using Azure.Provisioning.Primitives;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
Expand Down Expand Up @@ -1165,6 +1167,94 @@ param outputs_azure_container_apps_environment_id string
Assert.Equal(expectedBicep, bicep);
}

[Fact]
public async Task CanCustomizeWithProvisioningBuildOptions()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);

builder.Services.Configure<AzureProvisioningOptions>(options => options.ProvisioningBuildOptions.InfrastructureResolvers.Insert(0, new MyResourceNamePropertyResolver()));
builder.AddAzureContainerAppsInfrastructure();

builder.AddContainer("api1", "myimage");

using var app = builder.Build();

await ExecuteBeforeStartHooksAsync(app, default);

var model = app.Services.GetRequiredService<DistributedApplicationModel>();

var container = Assert.Single(model.GetContainerResources());

container.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);

var resource = target?.DeploymentTarget as AzureProvisioningResource;

Assert.NotNull(resource);

var (_, bicep) = await ManifestUtils.GetManifestWithBicep(resource);

var expectedBicep =
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param outputs_azure_container_registry_managed_identity_id string

param outputs_managed_identity_client_id string

param outputs_azure_container_apps_environment_id string

resource api1 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api1-my'
location: location
properties: {
configuration: {
activeRevisionsMode: 'Single'
}
environmentId: outputs_azure_container_apps_environment_id
template: {
containers: [
{
image: 'myimage:latest'
name: 'api1'
env: [
{
name: 'AZURE_CLIENT_ID'
value: outputs_managed_identity_client_id
}
]
}
]
scale: {
minReplicas: 1
}
}
}
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${outputs_azure_container_registry_managed_identity_id}': { }
}
}
}
""";
output.WriteLine(bicep);
Assert.Equal(expectedBicep, bicep);
}

private sealed class MyResourceNamePropertyResolver : DynamicResourceNamePropertyResolver
{
public override void ResolveProperties(ProvisionableConstruct construct, ProvisioningBuildOptions options)
{
if (construct is ContainerApp app)
{
app.Name = app.Name.Value + "-my";
}

base.ResolveProperties(construct, options);
}
}

[Fact]
public async Task ExternalEndpointBecomesIngress()
{
Expand Down