File: AzureKubernetesLoadBalancerResource.cs
Web Access
Project: src\src\Aspire.Hosting.Azure.Kubernetes\Aspire.Hosting.Azure.Kubernetes.csproj (Aspire.Hosting.Azure.Kubernetes)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIREPIPELINES001 // PipelineStepAnnotation/PipelineStep are evaluation-only
#pragma warning disable ASPIREAZURE003 // AzureSubnetResource is evaluation-only
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Pipelines;
 
namespace Aspire.Hosting.Azure.Kubernetes;
 
/// <summary>
/// Represents a single Azure Application Gateway for Containers (AGC)
/// <c>ApplicationLoadBalancer</c> Kubernetes custom resource (
/// <c>alb.networking.azure.io/v1</c>) bound to a delegated subnet.
/// </summary>
/// <remarks>
/// <para>
/// Each AGC <c>ApplicationLoadBalancer</c> is capped at 5 frontends, so larger
/// applications create multiple load balancer resources via repeated calls to
/// <see cref="AzureKubernetesEnvironmentExtensions.AddLoadBalancer"/> and associate
/// gateways/ingresses with a specific load balancer using
/// <see cref="AzureKubernetesIngressExtensions.WithLoadBalancer(global::Aspire.Hosting.ApplicationModel.IResourceBuilder{global::Aspire.Hosting.Kubernetes.KubernetesGatewayResource}, global::Aspire.Hosting.ApplicationModel.IResourceBuilder{AzureKubernetesLoadBalancerResource})"/>.
/// </para>
/// <para>
/// The resource registers a per-LB <c>apply-alb-crd-{name}</c> pipeline step that
/// runs after AKS credentials are fetched and before Helm chart preparation. The
/// step polls the cluster for the <c>azure-alb-external</c> GatewayClass (installed
/// by the AGC ALB controller add-on), then <c>kubectl apply</c>s the
/// <c>ApplicationLoadBalancer</c> custom resource pointing at the supplied subnet.
/// </para>
/// </remarks>
public sealed class AzureKubernetesLoadBalancerResource :
    Resource,
    IResourceWithParent<AzureKubernetesEnvironmentResource>
{
    /// <summary>
    /// Initializes a new instance of the <see cref="AzureKubernetesLoadBalancerResource"/> class.
    /// </summary>
    /// <param name="name">The name of the load balancer resource. Used to derive the in-cluster
    /// <c>ApplicationLoadBalancer</c> name (<c>alb-{name}</c>) referenced by gateway/ingress annotations.</param>
    /// <param name="parent">The parent AKS environment that owns this load balancer.</param>
    /// <param name="subnetIdReference">Reference to the resource ID of the delegated subnet
    /// this load balancer associates with. Resolved at deployment time by the
    /// <c>apply-alb-crd-{name}</c> pipeline step and emitted into the <c>spec.associations</c>
    /// field of the <c>ApplicationLoadBalancer</c> CR.</param>
    /// <param name="subnetResource">The Aspire subnet resource that backs this load balancer.</param>
    /// <param name="displacedDelegationServiceName">If non-<see langword="null"/>, the name of an
    /// existing subnet service delegation that <see cref="AzureKubernetesEnvironmentExtensions.AddLoadBalancer"/>
    /// silently overrode with the AGC <c>trafficControllers</c> delegation. The pipeline step logs
    /// a warning at deploy time so the user can investigate.</param>
    /// <remarks>
    /// All deploy-time state (<paramref name="subnetIdReference"/>, <paramref name="subnetResource"/>)
    /// is taken as a constructor parameter rather than via object-initializer setters because the
    /// constructor eagerly registers a <see cref="PipelineStepAnnotation"/> whose deferred action
    /// dereferences these fields. Using <c>= default!</c> properties with <c>{ get; set; }</c> would
    /// create a window where forgetting to set the property results in a runtime
    /// <see cref="NullReferenceException"/> at deploy with no compile-time signal. Compare
    /// <c>AzureContainerAppResource</c>, <c>AzureContainerRegistryResource</c>, and
    /// <c>AzureAppServiceWebSiteResource</c>, which follow the same pattern.
    /// </remarks>
    internal AzureKubernetesLoadBalancerResource(
        string name,
        AzureKubernetesEnvironmentResource parent,
        BicepOutputReference subnetIdReference,
        Aspire.Hosting.Azure.AzureSubnetResource subnetResource,
        string? displacedDelegationServiceName = null)
        : base(name)
    {
        ArgumentNullException.ThrowIfNull(parent);
        ArgumentNullException.ThrowIfNull(subnetIdReference);
        ArgumentNullException.ThrowIfNull(subnetResource);
 
        Parent = parent;
        SubnetIdReference = subnetIdReference;
        SubnetResource = subnetResource;
        DisplacedDelegationServiceName = displacedDelegationServiceName;
 
        // Register the per-LB pipeline step that applies the ApplicationLoadBalancer
        // CR into the cluster after credentials are available. Using a factory lambda
        // so the step is materialized lazily by the pipeline configuration phase.
        Annotations.Add(new PipelineStepAnnotation(_ =>
        {
            var step = new PipelineStep
            {
                Name = $"apply-alb-crd-{Name}",
                Description = $"Applies the AGC ApplicationLoadBalancer CR for {Name}.",
                Action = ctx => Parent.ApplyAlbCrdAsync(this, ctx),
                // Kubeconfig must be set first.
                DependsOnSteps = [$"aks-get-credentials-{Parent.Name}"],
                // Helm prepare can then reference the LB by name in gateway/ingress annotations.
                RequiredBySteps = [$"prepare-{Parent.KubernetesEnvironment.Name}"]
            };
 
            return Task.FromResult<IEnumerable<PipelineStep>>([step]);
        }));
    }
 
    /// <inheritdoc />
    public AzureKubernetesEnvironmentResource Parent { get; }
 
    /// <summary>
    /// Reference to the resource ID of the delegated subnet that this load balancer
    /// associates with. Resolved at deployment time and emitted into the
    /// <c>spec.associations</c> field of the <c>ApplicationLoadBalancer</c> CR.
    /// </summary>
    internal BicepOutputReference SubnetIdReference { get; }
 
    /// <summary>
    /// The Aspire subnet resource that backs this load balancer. Captured so the AKS
    /// environment's Bicep emission can synthesize a per-LB role assignment granting
    /// the AKS-auto-created AGC controller identity the
    /// <c>Microsoft.Network/virtualNetworks/subnets/join/action</c> permission on the
    /// subnet (via <c>Network Contributor</c>). Without this, AKS only auto-grants the
    /// AGC identity permissions inside the cluster's <c>MC_*</c> node resource group, so
    /// any user-supplied subnet outside that RG fails with <c>LinkedAuthorizationFailed</c>
    /// when the controller tries to create the AGC association.
    /// </summary>
    internal Aspire.Hosting.Azure.AzureSubnetResource SubnetResource { get; }
 
    /// <summary>
    /// The name of an existing subnet service delegation that <see cref="AzureKubernetesEnvironmentExtensions.AddLoadBalancer"/>
    /// silently overrode with the AGC <c>trafficControllers</c> delegation, or <see langword="null"/>
    /// if no override occurred. The deploy-time pipeline step logs a warning when this is set so the
    /// user is alerted that their original delegation will not be emitted into Bicep.
    /// </summary>
    internal string? DisplacedDelegationServiceName { get; }
 
    /// <summary>
    /// The in-cluster name of the <c>ApplicationLoadBalancer</c> CR. This is the value
    /// AGC expects in the <c>alb.networking.azure.io/alb-name</c> annotation on
    /// gateway/ingress resources.
    /// </summary>
    internal string AlbName => $"alb-{Name}";
 
    /// <summary>
    /// The Kubernetes namespace the <c>ApplicationLoadBalancer</c> CR is created in.
    /// Currently fixed to <c>default</c>; a future <c>WithNamespace</c> extension can
    /// surface this when multi-tenant clusters need isolation.
    /// </summary>
    internal static string AlbNamespace => "default";
}