Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple domains via ConfigureCustomDomain #7309

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
.WithEnvironment("VALUE", param)
.PublishAsAzureContainerApp((module, app) =>
{
app.ConfigureCustomDomain(customDomain, certificateName);
app.AddCustomDomain(customDomain, certificateName);

// Scale to 0
app.Template.Scale.MinReplicas = 0;
Expand Down
36 changes: 17 additions & 19 deletions src/Aspire.Hosting.Azure.AppContainers/ContainerAppExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,22 @@ namespace Aspire.Hosting;
public static class ContainerAppExtensions
{
/// <summary>
/// Configures the custom domain for the container app.
/// Adds a custom domain to the container app.
/// </summary>
/// <param name="app">The container app resource to configure for custom domain usage.</param>
/// <param name="app">The container app resource to add a custom domain.</param>
/// <param name="customDomain">A resource builder for a parameter resource capturing the name of the custom domain.</param>
/// <param name="certificateName">A resource builder for a parameter resource capturing the name of the certficate configured in the Azure Portal.</param>
/// <exception cref="ArgumentException">Throws if the container app resource is not parented to a <see cref="AzureResourceInfrastructure"/>.</exception>
/// <remarks>
/// <para>The <see cref="ConfigureCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/> extension method
/// <para>The <see cref="AddCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/> extension method
/// simplifies the process of assigning a custom domain to a container app resource when it is deployed. It has no impact on local development.</para>
/// <para>The <see cref="ConfigureCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/> method is used
/// <para>The <see cref="AddCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/> method is used
/// in conjunction with the <see cref="AzureContainerAppContainerExtensions.PublishAsAzureContainerApp{T}(IResourceBuilder{T}, Action{AzureResourceInfrastructure, ContainerApp})"/>
/// callback. Assigning a custom domain to a container app resource is a multi-step process and requires multiple deployments.</para>
/// <para>The <see cref="ConfigureCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/> method takes
/// <para>The <see cref="AddCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/> method takes
/// two arguments which are parameter resource builders. The first is a parameter that represents the custom domain and the second is a parameter that
/// represents the name of the managed certificate provisioned via the Azure Portal</para>
/// <para>When deploying with custom domains configured for the first time leave the <paramref name="certificateName"/> parameter empty (when prompted
/// <para>When deploying with custom domains added for the first time leave the <paramref name="certificateName"/> parameter empty (when prompted
/// by the Azure Developer CLI). Once the applicatio is deployed acucessfully access to the Azure Portal to bind the custom domain to a managed SSL
/// certificate. Once the certificate is successfully provisioned, subsequent deployments of the application can use this certificate name when the
/// <paramref name="certificateName"/> is prompted.</para>
Expand All @@ -40,7 +40,7 @@ public static class ContainerAppExtensions
/// </remarks>
/// <example>
/// This example shows declaring two parameters to capture the custom domain and certificate name and
/// passing them to the <see cref="ConfigureCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/>
/// passing them to the <see cref="AddCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/>
/// method via the <see cref="AzureContainerAppContainerExtensions.PublishAsAzureContainerApp{T}(IResourceBuilder{T}, Action{AzureResourceInfrastructure, ContainerApp})"/>
/// extension method.
/// <code lang="C#">
Expand All @@ -50,16 +50,16 @@ public static class ContainerAppExtensions
/// builder.AddProject&lt;Projects.InventoryService&gt;("inventory")
/// .PublishAsAzureContainerApp((module, app) =>
/// {
/// app.ConfigureCustomDomain(customDomain, certificateName);
/// app.AddCustomDomain(customDomain, certificateName);
/// });
/// </code>
/// </example>
[Experimental("ASPIREACADOMAINS001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static void ConfigureCustomDomain(this ContainerApp app, IResourceBuilder<ParameterResource> customDomain, IResourceBuilder<ParameterResource> certificateName)
public static void AddCustomDomain(this ContainerApp app, IResourceBuilder<ParameterResource> customDomain, IResourceBuilder<ParameterResource> certificateName)
{
if (app.ParentInfrastructure is not AzureResourceInfrastructure module)
{
throw new ArgumentException("Cannot configure custom domain when resource is not parented by ResourceModuleConstruct.", nameof(app));
throw new ArgumentException("Cannot add custom domain when resource is not parented by ResourceModuleConstruct.", nameof(app));
}

var containerAppManagedEnvironmentIdParameter = module.GetProvisionableResources().OfType<ProvisioningParameter>().Single(
Expand Down Expand Up @@ -90,14 +90,12 @@ public static void ConfigureCustomDomain(this ContainerApp app, IResourceBuilder
new NullLiteralExpression()
);

app.Configuration.Ingress.CustomDomains = new BicepList<ContainerAppCustomDomain>()
{
new ContainerAppCustomDomain()
{
BindingType = bindingTypeConditional,
Name = customDomainParameter,
CertificateId = certificateOrEmpty
}
};
app.Configuration.Ingress.CustomDomains.Add(
new ContainerAppCustomDomain
{
BindingType = bindingTypeConditional,
Name = customDomainParameter,
CertificateId = certificateOrEmpty
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ static Aspire.Hosting.AzureContainerAppContainerExtensions.PublishAsAzureContain
static Aspire.Hosting.AzureContainerAppExecutableExtensions.PublishAsAzureContainerApp<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! executable, System.Action<Aspire.Hosting.Azure.AzureResourceInfrastructure!, Azure.Provisioning.AppContainers.ContainerApp!>! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.AzureContainerAppExtensions.AddAzureContainerAppsInfrastructure(this Aspire.Hosting.IDistributedApplicationBuilder! builder) -> Aspire.Hosting.IDistributedApplicationBuilder!
static Aspire.Hosting.AzureContainerAppProjectExtensions.PublishAsAzureContainerApp<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! project, System.Action<Aspire.Hosting.Azure.AzureResourceInfrastructure!, Azure.Provisioning.AppContainers.ContainerApp!>! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ContainerAppExtensions.ConfigureCustomDomain(this Azure.Provisioning.AppContainers.ContainerApp! app, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>! customDomain, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>! certificateName) -> void
static Aspire.Hosting.ContainerAppExtensions.AddCustomDomain(this Azure.Provisioning.AppContainers.ContainerApp! app, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>! customDomain, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>! certificateName) -> void
135 changes: 133 additions & 2 deletions tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,7 @@ param outputs_azure_container_apps_environment_id string
}

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

Expand All @@ -1072,7 +1072,7 @@ public async Task ConfigureCustomDomainsMutatesIngress()
.WithHttpEndpoint(targetPort: 1111)
.PublishAsAzureContainerApp((module, c) =>
{
c.ConfigureCustomDomain(customDomain, certificateName);
c.AddCustomDomain(customDomain, certificateName);
});

using var app = builder.Build();
Expand Down Expand Up @@ -1175,6 +1175,137 @@ param customDomain string
Assert.Equal(expectedBicep, bicep);
}

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

var customDomain1 = builder.AddParameter("customDomain1");
var certificateName1 = builder.AddParameter("certificateName1");

var customDomain2 = builder.AddParameter("customDomain2");
var certificateName2 = builder.AddParameter("certificateName2");

builder.AddAzureContainerAppsInfrastructure();
builder.AddContainer("api", "myimage")
.WithHttpEndpoint(targetPort: 1111)
.PublishAsAzureContainerApp((module, c) =>
{
c.AddCustomDomain(customDomain1, certificateName1);
c.AddCustomDomain(customDomain2, certificateName2);
});

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 AzureBicepResource;

Assert.NotNull(resource);

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

var m = manifest.ToString();

var expectedManifest =
"""
{
"type": "azure.bicep.v0",
"path": "api.module.bicep",
"params": {
"outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}",
"outputs_managed_identity_client_id": "{.outputs.MANAGED_IDENTITY_CLIENT_ID}",
"outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}",
"certificateName1": "{certificateName1.value}",
"customDomain1": "{customDomain1.value}",
"certificateName2": "{certificateName2.value}",
"customDomain2": "{customDomain2.value}"
}
}
""";

Assert.Equal(expectedManifest, m);

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

param certificateName1 string

param customDomain1 string

param certificateName2 string

param customDomain2 string

resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
properties: {
configuration: {
activeRevisionsMode: 'Single'
ingress: {
external: false
targetPort: 1111
transport: 'http'
customDomains: [
{
name: customDomain1
bindingType: (certificateName1 != '') ? 'SniEnabled' : 'Disabled'
certificateId: (certificateName1 != '') ? '${outputs_azure_container_apps_environment_id}/managedCertificates/${certificateName1}' : null
}
{
name: customDomain2
bindingType: (certificateName2 != '') ? 'SniEnabled' : 'Disabled'
certificateId: (certificateName2 != '') ? '${outputs_azure_container_apps_environment_id}/managedCertificates/${certificateName2}' : null
}
]
}
}
environmentId: outputs_azure_container_apps_environment_id
template: {
containers: [
{
image: 'myimage:latest'
name: 'api'
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);
}

[Fact]
public async Task VolumesAndBindMountsAreTranslation()
{
Expand Down
Loading