|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | + |
| 4 | +using Aspire.Hosting.ApplicationModel; |
| 5 | +using Azure.Provisioning.AppContainers; |
| 6 | +using Azure.Provisioning.Expressions; |
| 7 | +using Azure.Provisioning; |
| 8 | +using System.Diagnostics.CodeAnalysis; |
| 9 | +using Aspire.Hosting.Azure; |
| 10 | + |
| 11 | +namespace Aspire.Hosting; |
| 12 | + |
| 13 | +/// <summary> |
| 14 | +/// Provides extension methods for customizing Azure Container App resource. |
| 15 | +/// </summary> |
| 16 | +public static class ContainerAppExtensions |
| 17 | +{ |
| 18 | + /// <summary> |
| 19 | + /// Configures the custom domain for the container app. |
| 20 | + /// </summary> |
| 21 | + /// <param name="app">The container app resource to configure for custom domain usage.</param> |
| 22 | + /// <param name="customDomain">A resource builder for a parameter resource capturing the name of the custom domain.</param> |
| 23 | + /// <param name="certificateName">A resource builder for a parameter resource capturing the name of the certficate configured in the Azure Portal.</param> |
| 24 | + /// <exception cref="ArgumentException">Throws if the container app resource is not parented to a <see cref="AzureResourceInfrastructure"/>.</exception> |
| 25 | + /// <remarks> |
| 26 | + /// <para>The <see cref="ConfigureCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/> extension method |
| 27 | + /// 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> |
| 28 | + /// <para>The <see cref="ConfigureCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/> method is used |
| 29 | + /// in conjunction with the <see cref="AzureContainerAppContainerExtensions.PublishAsAzureContainerApp{T}(IResourceBuilder{T}, Action{AzureResourceInfrastructure, ContainerApp})"/> |
| 30 | + /// callback. Assigning a custom domain to a container app resource is a multi-step process and requires multiple deployments.</para> |
| 31 | + /// <para>The <see cref="ConfigureCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/> method takes |
| 32 | + /// two arguments which are parameter resource builders. The first is a parameter that represents the custom domain and the second is a parameter that |
| 33 | + /// represents the name of the managed certificate provisioned via the Azure Portal</para> |
| 34 | + /// <para>When deploying with custom domains configured for the first time leave the <paramref name="certificateName"/> parameter empty (when prompted |
| 35 | + /// 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 |
| 36 | + /// certificate. Once the certificate is successfully provisioned, subsequent deployments of the application can use this certificate name when the |
| 37 | + /// <paramref name="certificateName"/> is prompted.</para> |
| 38 | + /// <para>For deployments triggered locally by the Azure Developer CLI the <c>config.json</c> file in the <c>.azure/{environment name}</c> path |
| 39 | + /// can by modified with the certificate name since Azure Developer CLI will not prompt again for the value.</para> |
| 40 | + /// </remarks> |
| 41 | + /// <example> |
| 42 | + /// This example shows declaring two parameters to capture the custom domain and certificate name and |
| 43 | + /// passing them to the <see cref="ConfigureCustomDomain(ContainerApp, IResourceBuilder{ParameterResource}, IResourceBuilder{ParameterResource})"/> |
| 44 | + /// method via the <see cref="AzureContainerAppContainerExtensions.PublishAsAzureContainerApp{T}(IResourceBuilder{T}, Action{AzureResourceInfrastructure, ContainerApp})"/> |
| 45 | + /// extension method. |
| 46 | + /// <code lang="C#"> |
| 47 | + /// var builder = DistributedApplication.CreateBuilder(); |
| 48 | + /// var customDomain = builder.AddParameter("customDomain"); // Value provided at first deployment. |
| 49 | + /// var certificateName = builder.AddParameter("certificateName"); // Value provided at second and subsequent deployments. |
| 50 | + /// builder.AddProject<Projects.InventoryService>("inventory") |
| 51 | + /// .PublishAsAzureContainerApp((module, app) => |
| 52 | + /// { |
| 53 | + /// app.ConfigureCustomDomain(customDomain, certificateName); |
| 54 | + /// }); |
| 55 | + /// </code> |
| 56 | + /// </example> |
| 57 | + [Experimental("ASPIREACADOMAINS001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")] |
| 58 | + public static void ConfigureCustomDomain(this ContainerApp app, IResourceBuilder<ParameterResource> customDomain, IResourceBuilder<ParameterResource> certificateName) |
| 59 | + { |
| 60 | + if (app.ParentInfrastructure is not AzureResourceInfrastructure module) |
| 61 | + { |
| 62 | + throw new ArgumentException("Cannot configure custom domain when resource is not parented by ResourceModuleConstruct.", nameof(app)); |
| 63 | + } |
| 64 | + |
| 65 | + var containerAppManagedEnvironmentIdParameter = module.GetResources().OfType<ProvisioningParameter>().Single( |
| 66 | + p => p.IdentifierName == "outputs_azure_container_apps_environment_id"); |
| 67 | + var certificatNameParameter = certificateName.AsProvisioningParameter(module); |
| 68 | + var customDomainParameter = customDomain.AsProvisioningParameter(module); |
| 69 | + |
| 70 | + var bindingTypeConditional = new ConditionalExpression( |
| 71 | + new BinaryExpression( |
| 72 | + new IdentifierExpression(certificatNameParameter.IdentifierName), |
| 73 | + BinaryOperator.NotEqual, |
| 74 | + new StringLiteral(string.Empty)), |
| 75 | + new StringLiteral("SniEnabled"), |
| 76 | + new StringLiteral("Disabled") |
| 77 | + ); |
| 78 | + |
| 79 | + var certificateOrEmpty = new ConditionalExpression( |
| 80 | + new BinaryExpression( |
| 81 | + new IdentifierExpression(certificatNameParameter.IdentifierName), |
| 82 | + BinaryOperator.NotEqual, |
| 83 | + new StringLiteral(string.Empty)), |
| 84 | + new InterpolatedString( |
| 85 | + "{0}/managedCertificates/{1}", |
| 86 | + [ |
| 87 | + new IdentifierExpression(containerAppManagedEnvironmentIdParameter.IdentifierName), |
| 88 | + new IdentifierExpression(certificatNameParameter.IdentifierName) |
| 89 | + ]), |
| 90 | + new NullLiteral() |
| 91 | + ); |
| 92 | + |
| 93 | + app.Configuration.Value!.Ingress!.Value!.CustomDomains = new BicepList<ContainerAppCustomDomain>() |
| 94 | + { |
| 95 | + new ContainerAppCustomDomain() |
| 96 | + { |
| 97 | + BindingType = bindingTypeConditional, |
| 98 | + Name = new IdentifierExpression(customDomainParameter.IdentifierName), |
| 99 | + CertificateId = certificateOrEmpty |
| 100 | + } |
| 101 | + }; |
| 102 | + } |
| 103 | +} |
0 commit comments