File: AzureInternalLoadBalancerExtensions.cs
Web Access
Project: src\src\Aspire.Hosting.Azure.Network\Aspire.Hosting.Azure.Network.csproj (Aspire.Hosting.Azure.Network)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.IO.Hashing;
using System.Text;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Provides extension methods for configuring internal load balancers and private DNS.
/// </summary>
public static class AzureInternalLoadBalancerExtensions
{
    /// <summary>
    /// Configures the resource with an internal load balancer and private DNS.
    /// </summary>
    /// <typeparam name="T">The resource type that supports an internal load balancer.</typeparam>
    /// <param name="builder">The resource builder.</param>
    /// <param name="virtualNetwork">The virtual network containing the resource's delegated subnet.</param>
    /// <returns>The resource builder.</returns>
    /// <remarks>
    /// The resource must first be configured with <c>WithDelegatedSubnet</c>. A private DNS zone named
    /// after its default domain is linked to <paramref name="virtualNetwork"/>, and a wildcard record
    /// resolves hostnames to the internal load balancer's static IP. This method has no effect in run mode.
    /// </remarks>
    /// <exception cref="ArgumentNullException">Thrown when an argument is null.</exception>
    /// <exception cref="InvalidOperationException">Thrown when the delegated subnet is missing, belongs to a different network, or the resource already uses a different internal network.</exception>
    [AspireExport("withNetworkInternalLoadBalancer", MethodName = "withInternalLoadBalancer")]
    public static IResourceBuilder<T> WithInternalLoadBalancer<T>(
        this IResourceBuilder<T> builder,
        IResourceBuilder<AzureVirtualNetworkResource> virtualNetwork)
        where T : IAzureInternalIngressResource
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(virtualNetwork);
 
        if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
        {
            return builder;
        }
 
        if (!builder.Resource.TryGetLastAnnotation<DelegatedSubnetAnnotation>(out var delegatedSubnet))
        {
            throw new InvalidOperationException(
                $"Azure resource '{builder.Resource.Name}' must use a delegated subnet before it can use an internal load balancer.");
        }
 
        var subnet = builder.ApplicationBuilder.Resources
            .OfType<AzureSubnetResource>()
            .SingleOrDefault(candidate =>
                ReferenceExpression.Create($"{candidate.Id}").ValueExpression == delegatedSubnet.SubnetId.ValueExpression);
        if (subnet is null || !ReferenceEquals(subnet.Parent, virtualNetwork.Resource))
        {
            throw new InvalidOperationException(
                $"The delegated subnet for Azure resource '{builder.Resource.Name}' must belong to virtual network '{virtualNetwork.Resource.Name}'.");
        }
 
        if (builder.Resource.TryGetLastAnnotation<InternalLoadBalancerAnnotation>(out var existing))
        {
            if (!ReferenceEquals(existing.VirtualNetwork, virtualNetwork.Resource))
            {
                throw new InvalidOperationException(
                    $"Azure resource '{builder.Resource.Name}' is already linked to virtual network '{existing.VirtualNetwork.Name}'.");
            }
 
            return builder;
        }
 
        builder.WithAnnotation(new InternalLoadBalancerAnnotation(virtualNetwork.Resource));
        var privateDnsResourceName = GetBoundedResourceName($"{builder.Resource.Name}-private-dns");
 
        var privateDns = new AzurePrivateDnsZoneResource(privateDnsResourceName, builder.Resource.DefaultDomain);
        privateDns.ARecords.Add(("*", builder.Resource.StaticIp));
 
        var vnetLink = new AzurePrivateDnsZoneVNetLinkResource(
            GetBoundedResourceName($"{privateDnsResourceName}-{virtualNetwork.Resource.Name}-link"),
            privateDns,
            virtualNetwork.Resource);
        privateDns.VNetLinks.Add(virtualNetwork.Resource, vnetLink);
        builder.ApplicationBuilder.AddResource(vnetLink).ExcludeFromManifest();
 
        builder.ApplicationBuilder.AddAzureProvisioning();
        builder.ApplicationBuilder
            .AddResource(privateDns)
            .ConfigureInfrastructure(_ => ValidateSubnetDelegation(builder.Resource, subnet))
            .WithParentRelationship(builder.Resource)
            .WithRelationship(virtualNetwork.Resource, "Virtual network link");
 
        return builder.WithRelationship(virtualNetwork.Resource, "Internal network");
    }
 
    private static string GetBoundedResourceName(string name)
    {
        if (name.Length <= 64)
        {
            return name;
        }
 
        var hash = Convert.ToHexString(XxHash3.Hash(Encoding.UTF8.GetBytes(name))).ToLowerInvariant()[..8];
        return $"{name[..55]}-{hash}";
    }
 
    private static void ValidateSubnetDelegation(
        IAzureInternalIngressResource resource,
        AzureSubnetResource subnet)
    {
        // WithServiceDelegation is explicitly last-write-wins, so the subnet delegation validated at
        // WithInternalLoadBalancer call time can be replaced afterward by an unrelated later call on the
        // same subnet (for example another WithDelegatedSubnet target, or a direct WithServiceDelegation
        // call). Revalidate the delegation here, during infrastructure generation, so call order cannot
        // emit this resource with IsInternal = true (and this private DNS module) while its subnet is no
        // longer delegated to the resource's required service.
        subnet.TryGetLastAnnotation<AzureSubnetServiceDelegationAnnotation>(out var delegation);
        if (delegation is null || !string.Equals(
                delegation.ServiceName,
                resource.DelegatedSubnetServiceName,
                StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException(
                $"The delegated subnet for Azure resource '{resource.Name}' must remain delegated to " +
                $"'{resource.DelegatedSubnetServiceName}' for an internal load balancer, but it is " +
                $"{(delegation is null ? "no longer delegated" : $"delegated to '{delegation.ServiceName}'")}.");
        }
    }
}