File: AzureKubernetesEnvironmentExtensionsTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Azure.Kubernetes.Tests\Aspire.Hosting.Azure.Kubernetes.Tests.csproj (Aspire.Hosting.Azure.Kubernetes.Tests)
// 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 ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIRECOMPUTE003 // Type is for evaluation purposes only
 
using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure.Kubernetes;
using Aspire.Hosting.Kubernetes;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Utils;
using Azure.Provisioning.ContainerService;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class AzureKubernetesEnvironmentExtensionsTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task AddAzureKubernetesEnvironment_BasicConfiguration()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        Assert.Equal("aks", aks.Resource.Name);
        Assert.Equal("{aks.outputs.id}", aks.Resource.Id.ValueExpression);
        Assert.Equal("{aks.outputs.clusterFqdn}", aks.Resource.ClusterFqdn.ValueExpression);
        Assert.Equal("{aks.outputs.oidcIssuerUrl}", aks.Resource.OidcIssuerUrl.ValueExpression);
 
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        await Verify(manifest.BicepText, extension: "bicep");
    }
 
    [Fact]
    public async Task AddAzureKubernetesEnvironment_ConfigureInfrastructureCanSelectFreeTier()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .ConfigureInfrastructure(infrastructure =>
            {
                var cluster = infrastructure.GetProvisionableResources()
                    .OfType<ContainerServiceManagedCluster>()
                    .Single();
 
                cluster.Sku.Tier = ManagedClusterSkuTier.Free;
            });
 
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        await Verify(manifest.BicepText, extension: "bicep");
    }
 
    [Fact]
    public void AddNodePool_ReturnsNodePoolResource()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var gpuPool = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5);
 
        // Default system pool + added user pool
        Assert.Equal(2, aks.Resource.NodePools.Count);
 
        Assert.Equal("gpu", gpuPool.Resource.Name);
        Assert.Equal("gpu", gpuPool.Resource.Config.Name);
        Assert.Equal("Standard_NC6s_v3", gpuPool.Resource.Config.VmSize);
        Assert.Equal(0, gpuPool.Resource.Config.MinCount);
        Assert.Equal(5, gpuPool.Resource.Config.MaxCount);
        Assert.Equal(AksNodePoolMode.User, gpuPool.Resource.Config.Mode);
        Assert.Null(gpuPool.Resource.Config.OsSku);
        Assert.Same(aks.Resource, gpuPool.Resource.AksParent);
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_DefaultNodePool()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        Assert.Single(aks.Resource.NodePools);
        var defaultPool = aks.Resource.NodePools[0];
        Assert.Equal("system", defaultPool.Name);
        Assert.Equal("Standard_D2s_v5", defaultPool.VmSize);
        Assert.Equal(1, defaultPool.MinCount);
        Assert.Equal(3, defaultPool.MaxCount);
        Assert.Equal(AksNodePoolMode.System, defaultPool.Mode);
        Assert.Null(defaultPool.OsSku);
    }
 
    [Theory]
    [InlineData(DistributedApplicationOperation.Run)]
    [InlineData(DistributedApplicationOperation.Publish)]
    public void NodePoolCallbacks_ConfigureIndependentPools(DistributedApplicationOperation operation)
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace, operation);
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        var result = aks.WithSystemNodePool(pool =>
        {
            pool.VmSize = "Standard_D4s_v5";
            pool.MinCount = 2;
            pool.MaxCount = 4;
            pool.OsSku = "AzureLinux";
        });
        var workers = aks.AddNodePool("workers", pool =>
        {
            pool.MinCount = 0;
            pool.MaxCount = 5;
            pool.OsSku = "Ubuntu";
        });
        var defaults = aks.AddNodePool("defaults", _ => { });
 
        Assert.Same(aks, result);
        Assert.Collection(aks.Resource.NodePools,
            pool => Assert.Equal(new AksNodePoolConfig("system", "Standard_D4s_v5", 2, 4, AksNodePoolMode.System, "AzureLinux"), pool),
            pool => Assert.Same(workers.Resource.Config, pool),
            pool => Assert.Same(defaults.Resource.Config, pool));
        Assert.Equal(new AksNodePoolConfig("workers", "Standard_D2s_v5", 0, 5, AksNodePoolMode.User, "Ubuntu"), workers.Resource.Config);
        Assert.Equal(new AksNodePoolConfig("defaults", "Standard_D2s_v5", 1, 3, AksNodePoolMode.User), defaults.Resource.Config);
    }
 
    [Fact]
    public async Task NodePoolCallbacks_ComposeConfigurationUntilBicepGeneration()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        AksNodePoolConfig? systemConfig = null;
        AksNodePoolConfig? workerConfig = null;
        aks.WithSystemNodePool(pool =>
        {
            systemConfig = pool;
            pool.MinCount = -1;
            pool.OsSku = "AzureLinux";
        });
        var workers = aks.AddNodePool("workers", pool =>
        {
            workerConfig = pool;
            pool.MinCount = -1;
            pool.OsSku = "Ubuntu";
        });
 
        Assert.NotNull(systemConfig);
        Assert.NotNull(workerConfig);
        Assert.Same(systemConfig, aks.Resource.NodePools[0]);
        Assert.Same(workerConfig, workers.Resource.Config);
        Assert.NotSame(systemConfig, workerConfig);
        systemConfig.MinCount = 1;
        systemConfig.OsSku = "Ubuntu";
        workers.Resource.Config.OsSku = "AzureLinux";
        workers.Resource.Config.VmSize = "Standard_D4s_v5";
        workers.Resource.Config.MinCount = 2;
        workers.Resource.Config.MaxCount = 4;
 
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
 
        systemConfig.OsSku = "AzureLinux";
        workerConfig.OsSku = "Ubuntu";
        workerConfig.MinCount = 0;
        var regenerated = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
 
        await Verify(new
        {
            Initial = manifest.BicepText,
            Regenerated = regenerated.BicepText
        });
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void WithSystemNodePool_SubsequentCallResetsOptions(bool useCallback)
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool(pool => pool.OsSku = "AzureLinux");
        var workers = aks.AddNodePool("workers", pool => pool.OsSku = "Ubuntu");
 
        if (useCallback)
        {
            aks.WithSystemNodePool(_ => { });
        }
        else
        {
            aks.WithSystemNodePool();
        }
 
        Assert.Collection(aks.Resource.NodePools,
            pool => Assert.Equal(new AksNodePoolConfig("system", "Standard_D2s_v5", 1, 3, AksNodePoolMode.System), pool),
            pool => Assert.Same(workers.Resource.Config, pool));
    }
 
    [Theory]
    [InlineData(null, 1, 3, null)]
    [InlineData("", 1, 3, null)]
    [InlineData(" ", 1, 3, null)]
    [InlineData("Standard_D2s_v5", -1, 3, null)]
    [InlineData("Standard_D2s_v5", 1, -1, null)]
    [InlineData("Standard_D2s_v5", 4, 3, null)]
    [InlineData("Standard_D2s_v5", 1, 3, "")]
    [InlineData("Standard_D2s_v5", 1, 3, " ")]
    public async Task NodePoolConfiguration_ValidatesFinalValuesDuringBicepGeneration(string? vmSize, int minCount, int maxCount, string? osSku)
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        void Configure(AksNodePoolConfig pool)
        {
            pool.VmSize = vmSize!;
            pool.MinCount = minCount;
            pool.MaxCount = maxCount;
            pool.OsSku = osSku;
        }
 
        aks.WithSystemNodePool(_ => { });
        var workers = aks.AddNodePool("workers", _ => { });
        Configure(aks.Resource.NodePools[0]);
        Configure(workers.Resource.Config);
 
        await Assert.ThrowsAnyAsync<ArgumentException>(() => AzureManifestUtils.GetManifestWithBicep(aks.Resource));
 
        aks.WithSystemNodePool();
        await Assert.ThrowsAnyAsync<ArgumentException>(() => AzureManifestUtils.GetManifestWithBicep(aks.Resource));
    }
 
    [Fact]
    public void NodePoolCallbacks_RejectNullArguments()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var original = Assert.Single(aks.Resource.NodePools);
        IResourceBuilder<AzureKubernetesEnvironmentResource> nullBuilder = null!;
 
        Assert.Throws<ArgumentNullException>(() => aks.WithSystemNodePool((Action<AksNodePoolConfig>)null!));
        Assert.Throws<ArgumentNullException>(() => aks.AddNodePool("workers", (Action<AksNodePoolConfig>)null!));
        Assert.Throws<ArgumentNullException>(() => nullBuilder.WithSystemNodePool(_ => { }));
        Assert.Throws<ArgumentNullException>(() => nullBuilder.AddNodePool("workers", _ => { }));
        Assert.Throws<ArgumentException>(() => aks.AddNodePool("", _ => { }));
        Assert.Same(original, Assert.Single(aks.Resource.NodePools));
    }
 
    [Fact]
    public async Task WithSystemNodePool_RejectsZeroCountsDuringBicepGeneration()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        aks.WithSystemNodePool(pool => pool.MinCount = 0);
        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => AzureManifestUtils.GetManifestWithBicep(aks.Resource));
 
        aks.WithSystemNodePool(pool => pool.MaxCount = 0);
        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => AzureManifestUtils.GetManifestWithBicep(aks.Resource));
    }
 
    [Fact]
    public void NodePoolCallbacks_PropagateExceptionsWithoutMutatingModel()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var original = Assert.Single(aks.Resource.NodePools);
        var error = new InvalidOperationException("Callback failed.");
 
        Assert.Same(error, Assert.Throws<InvalidOperationException>(() => aks.WithSystemNodePool(_ => throw error)));
        Assert.Same(error, Assert.Throws<InvalidOperationException>(() => aks.AddNodePool("workers", _ => throw error)));
        Assert.Same(original, Assert.Single(aks.Resource.NodePools));
    }
 
    [Theory]
    [InlineData(null, null)]
    [InlineData("AzureLinux", "AzureLinux")]
    [InlineData("AzureLinux", "Ubuntu")]
    [InlineData(null, "FutureLinuxSku")]
    public async Task NodePoolCallbacks_BicepReflectsOsSku(string? systemOsSku, string? workerOsSku)
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool(pool => pool.OsSku = systemOsSku);
        aks.AddNodePool("workers", pool => pool.OsSku = workerOsSku);
        aks.AddNodePool("defaults");
 
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        var regenerated = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        Assert.Equal(manifest.BicepText, regenerated.BicepText);
        await Verify(manifest.BicepText, extension: "bicep")
            .UseParameters(systemOsSku, workerOsSku);
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_DefaultConfiguration()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        Assert.True(aks.Resource.OidcIssuerEnabled);
        Assert.True(aks.Resource.WorkloadIdentityEnabled);
        Assert.Null(aks.Resource.KubernetesVersion);
        Assert.False(aks.Resource.IsPrivateCluster);
        Assert.False(aks.Resource.ContainerInsightsEnabled);
        Assert.Null(aks.Resource.LogAnalyticsWorkspace);
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_HasInternalKubernetesEnvironment()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        Assert.NotNull(aks.Resource.KubernetesEnvironment);
        Assert.Equal("aks", aks.Resource.KubernetesEnvironment.Name);
        Assert.True(aks.Resource.TryGetLastAnnotation<KubernetesEnvironmentAnnotation>(out var annotation));
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_AddsOnlyAksComputeEnvironmentToModel()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        Assert.DoesNotContain(model.Resources, r => r is KubernetesEnvironmentResource);
 
        var computeEnvironment = Assert.Single(model.Resources.OfType<IComputeEnvironmentResource>());
        Assert.Same(aks.Resource, computeEnvironment);
    }
 
    [Fact]
    public async Task AddAzureKubernetesEnvironment_AllowsKubernetesServiceCustomizationWithoutVisibleKubernetesEnvironment()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        builder.AddAzureKubernetesEnvironment("aks");
        builder.AddContainer("api", "myimage")
            .PublishAsKubernetesService(_ => { });
 
        await using var app = builder.Build();
 
        await ExecuteBeforeStartHooksAsync(app, default);
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_ThrowsOnNullBuilder()
    {
        IDistributedApplicationBuilder builder = null!;
 
        Assert.Throws<ArgumentNullException>(() =>
            builder.AddAzureKubernetesEnvironment("aks"));
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_ThrowsOnEmptyName()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        Assert.Throws<ArgumentException>(() =>
            builder.AddAzureKubernetesEnvironment(""));
    }
 
    [Fact]
    public void WithWorkloadIdentity_EnablesOidcAndWorkloadIdentity()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithWorkloadIdentity();
 
        Assert.True(aks.Resource.OidcIssuerEnabled);
        Assert.True(aks.Resource.WorkloadIdentityEnabled);
    }
 
    [Fact]
    public void WithAzureUserAssignedIdentity_WorksWithAks()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var identity = builder.AddAzureUserAssignedIdentity("myIdentity");
 
        var project = builder.AddContainer("myapi", "myimage")
            .WithAzureUserAssignedIdentity(identity);
 
        Assert.True(project.Resource.TryGetLastAnnotation<AppIdentityAnnotation>(out var appIdentity));
        Assert.Same(identity.Resource, appIdentity.IdentityResource);
    }
 
    [Fact]
    public void AzureKubernetesEnvironment_ImplementsIAzureComputeEnvironmentResource()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        Assert.IsAssignableFrom<IAzureComputeEnvironmentResource>(aks.Resource);
    }
 
    [Fact]
    public void AzureKubernetesEnvironment_ImplementsIAzureNspAssociationTarget()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        Assert.IsAssignableFrom<IAzureNspAssociationTarget>(aks.Resource);
    }
 
    [Fact]
    public void AsExisting_WorksOnAksResource()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var nameParam = builder.AddParameter("aks-name");
        var rgParam = builder.AddParameter("aks-rg");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .AsExisting(nameParam, rgParam);
 
        Assert.NotNull(aks);
    }
 
    [Fact]
    public void WithSubnet_OnNodePool_StoresPerPoolSubnet()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var defaultSubnet = vnet.AddSubnet("default-subnet", "10.0.0.0/22");
        var gpuSubnet = vnet.AddSubnet("gpu-subnet", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSubnet(defaultSubnet);
 
        var gpuPool = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5)
            .WithSubnet(gpuSubnet);
 
        // Environment-level subnet should be set via annotation
        Assert.True(aks.Resource.TryGetLastAnnotation<AksSubnetAnnotation>(out _));
 
        // Per-pool subnet should be stored in NodePoolSubnets dictionary
        Assert.Single(aks.Resource.NodePoolSubnets);
        Assert.True(aks.Resource.NodePoolSubnets.ContainsKey("gpu"));
 
        // Node pool should also have its own subnet annotation
        Assert.True(gpuPool.Resource.TryGetLastAnnotation<AksSubnetAnnotation>(out _));
    }
 
    [Fact]
    public void WithSubnet_OnNodePool_WithoutEnvironmentSubnet()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var gpuSubnet = vnet.AddSubnet("gpu-subnet", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        // Only set subnet on the pool, not the environment
        var gpuPool = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5)
            .WithSubnet(gpuSubnet);
 
        // No environment-level subnet
        Assert.False(aks.Resource.TryGetLastAnnotation<AksSubnetAnnotation>(out _));
 
        // Per-pool subnet should still work
        Assert.Single(aks.Resource.NodePoolSubnets);
        Assert.True(aks.Resource.NodePoolSubnets.ContainsKey("gpu"));
    }
 
    [Fact]
    public void WithNodePool_AddsAnnotation()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var gpuPool = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5);
 
        var container = builder.AddContainer("myapi", "myimage")
            .WithNodePool(gpuPool);
 
        Assert.True(container.Resource.TryGetLastAnnotation<KubernetesNodePoolAnnotation>(out var affinity));
        Assert.Same(gpuPool.Resource, affinity.NodePool);
        Assert.Equal("gpu", affinity.NodePool.Name);
    }
 
    [Fact]
    public void AddNodePool_MultiplePoolsSupported()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var pool1 = aks.AddNodePool("cpu", "Standard_D2s_v5", 1, 10);
        var pool2 = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5);
 
        // Default system pool + 2 user pools
        Assert.Equal(3, aks.Resource.NodePools.Count);
        Assert.Equal("cpu", pool1.Resource.Name);
        Assert.Equal("gpu", pool2.Resource.Name);
    }
 
    [Fact]
    public void AddAzureKubernetesEnvironment_AutoCreatesDefaultRegistry()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        Assert.NotNull(aks.Resource.DefaultContainerRegistry);
        Assert.Equal("aks-acr", aks.Resource.DefaultContainerRegistry.Name);
    }
 
    [Fact]
    public void WithContainerRegistry_ReplacesDefault()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var explicitAcr = builder.AddAzureContainerRegistry("my-acr");
 
        aks.WithContainerRegistry(explicitAcr);
 
        // Default registry should be removed
        Assert.Null(aks.Resource.DefaultContainerRegistry);
 
        // Explicit registry should be set via annotation
        Assert.True(aks.Resource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var annotation));
        Assert.Same(explicitAcr.Resource, annotation.Registry);
    }
 
    [Fact]
    public async Task ContainerRegistry_FlowsToInnerKubernetesEnvironment()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var container = builder.AddContainer("myapi", "myimage");
 
        await using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
 
        Assert.True(aks.Resource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var aksAnnotation));
        Assert.Same(aks.Resource.DefaultContainerRegistry, aksAnnotation.Registry);
 
        Assert.True(aks.Resource.KubernetesEnvironment
            .TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var annotation));
        Assert.Same(aks.Resource.DefaultContainerRegistry, annotation.Registry);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task WithContainerRegistry_CrossResourceGroupPublishesScopedKubeletRole(bool parameterizedScope)
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var registry = builder.AddAzureContainerRegistry("registry");
        if (parameterizedScope)
        {
            registry.PublishAsExistingInResourceGroup(
                builder.AddParameter("registry-name", "existingacr"),
                builder.AddParameter("registry-group", "registry-rg"),
                builder.AddParameter("registry-subscription", "00000000-0000-0000-0000-000000000001"));
        }
        else
        {
            registry.PublishAsExisting("existingacr", "registry-rg");
        }
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithContainerRegistry(registry);
        builder.AddContainer("api", "myimage").WithHttpEndpoint(targetPort: 8080);
 
        await using var app = builder.Build();
        await app.RunAsync();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var roles = Assert.Single(model.Resources.OfType<AzureRoleAssignmentResource>(),
            resource => resource.Name == "aks-kubelet-identity-roles-registry");
        Assert.Same(registry.Resource, roles.TargetAzureResource);
        Assert.Same(aks.Resource.KubeletIdentity, roles.IdentityResource);
        Assert.Equal(aks.Resource.KubeletIdentity!.PrincipalId.ValueExpression,
            Assert.IsType<BicepOutputReference>(roles.Parameters["principalId"]).ValueExpression);
        Assert.NotNull(roles.Scope);
        Assert.False(aks.Resource.Parameters.ContainsKey("acrName"));
        Assert.Equal("{aks.outputs.kubeletIdentityObjectId}", aks.Resource.KubeletIdentityObjectId.ValueExpression);
 
        var mainBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "main.bicep"));
        var aksBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "aks", "aks.bicep"));
        var roleBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, roles.Name, $"{roles.Name}.bicep"));
        var registryBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "registry", "registry.bicep"));
        var controlPlaneBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "aks-control-plane-identity", "aks-control-plane-identity.bicep"));
        var kubeletBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "aks-kubelet-identity", "aks-kubelet-identity.bicep"));
        var operatorBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "aks-control-plane-identity-roles-aks-kubelet-identity", "aks-control-plane-identity-roles-aks-kubelet-identity.bicep"));
        await Verify(mainBicep, "bicep")
            .UseParameters(parameterizedScope)
            .AppendContentAsFile(aksBicep, "bicep")
            .AppendContentAsFile(roleBicep, "bicep")
            .AppendContentAsFile(registryBicep, "bicep")
            .AppendContentAsFile(controlPlaneBicep, "bicep")
            .AppendContentAsFile(kubeletBicep, "bicep")
            .AppendContentAsFile(operatorBicep, "bicep");
    }
 
    [Fact]
    public async Task WithContainerRegistry_CrossResourceGroupUsesFinalRegistryAndPreparesOnce()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var first = builder.AddAzureContainerRegistry("first").PublishAsExisting("firstacr", "first-rg");
        var final = builder.AddAzureContainerRegistry("final");
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithContainerRegistry(first)
            .WithContainerRegistry(final);
        final.PublishAsExisting("finalacr", "final-rg");
 
        await using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var provisioningRoles = Assert.Single(model.Resources.OfType<AzureRoleAssignmentResource>(),
            resource => resource.Name == "aks-kubelet-identity-roles-final");
        Assert.Equal("final-rg", provisioningRoles.Scope!.ResourceGroup);
        Assert.Same(final.Resource, provisioningRoles.TargetAzureResource);
        Assert.Same(aks.Resource.KubeletIdentity, provisioningRoles.IdentityResource);
        Assert.Collection(model.Resources.OfType<AzureUserAssignedIdentityResource>(),
            identity => Assert.Same(aks.Resource.ControlPlaneIdentity, identity),
            identity => Assert.Same(aks.Resource.KubeletIdentity, identity));
    }
 
    [Fact]
    public async Task WithContainerRegistry_SharedCrossGroupRegistryGrantsEachKubeletRole()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var registry = builder.AddAzureContainerRegistry("registry").PublishAsExisting("existingacr", "registry-rg");
        var first = builder.AddAzureKubernetesEnvironment("first").WithContainerRegistry(registry);
        var second = builder.AddAzureKubernetesEnvironment("second").WithContainerRegistry(registry);
 
        await using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
        var roles = app.Services.GetRequiredService<DistributedApplicationModel>().Resources
            .OfType<AzureRoleAssignmentResource>().Where(resource => resource.TargetAzureResource == registry.Resource);
        Assert.Collection(roles,
            role => Assert.Same(first.Resource.KubeletIdentity, role.IdentityResource),
            role => Assert.Same(second.Resource.KubeletIdentity, role.IdentityResource));
    }
 
    [Fact]
    public async Task WithContainerRegistry_UnscopedExistingRegistryUsesKubeletIdentity()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var registry = builder.AddAzureContainerRegistry("registry").PublishAsExisting("existingacr", resourceGroup: null);
        var aks = builder.AddAzureKubernetesEnvironment("aks").WithContainerRegistry(registry);
 
        await using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var roles = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>().Resources
            .OfType<AzureRoleAssignmentResource>(), resource => resource.TargetAzureResource == registry.Resource);
        Assert.Same(aks.Resource.KubeletIdentity, roles.IdentityResource);
        Assert.Null(roles.Scope);
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        await Verify(manifest.BicepText, "bicep");
    }
 
    [Fact]
    public async Task WithContainerRegistry_RunModeDoesNotProvisionClusterIdentities()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper, workspace, operation: DistributedApplicationOperation.Run);
        var registry = builder.AddAzureContainerRegistry("registry").RunAsExisting("existingacr", "registry-rg");
        var aks = builder.AddAzureKubernetesEnvironment("aks").WithContainerRegistry(registry);
 
        Assert.Null(aks.Resource.ControlPlaneIdentity);
        Assert.Null(aks.Resource.KubeletIdentity);
        Assert.Empty(builder.Resources.OfType<AzureUserAssignedIdentityResource>());
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        await Verify(manifest.BicepText, "bicep");
    }
 
    [Fact]
    public async Task DefaultRegistry_GrantsPullOnlyToKubeletAndOperatorOnlyToControlPlane()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        await using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var roles = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<AzureRoleAssignmentResource>();
        Assert.Collection(roles,
            role =>
            {
                Assert.Same(aks.Resource.ControlPlaneIdentity, role.IdentityResource);
                Assert.Same(aks.Resource.KubeletIdentity, role.TargetAzureResource);
                Assert.Contains(role, aks.Resource.References);
            },
            role =>
            {
                Assert.Same(aks.Resource.KubeletIdentity, role.IdentityResource);
                Assert.Same(aks.Resource.DefaultContainerRegistry, role.TargetAzureResource);
                Assert.Contains(role, aks.Resource.References);
            });
        Assert.Collection(aks.Resource.KubeletIdentity!.Annotations.OfType<RoleAssignmentAnnotation>(),
            annotation => Assert.Collection(annotation.Roles,
                role => Assert.Equal(new RoleDefinition("7f951dda-4ed3-4680-a7ca-43fe172d538d", "AcrPull"), role)));
        Assert.Collection(aks.Resource.ControlPlaneIdentity!.Annotations.OfType<RoleAssignmentAnnotation>(),
            annotation => Assert.Collection(annotation.Roles,
                role => Assert.Equal(new RoleDefinition("f1a07417-d97a-45cb-824c-7a7467783830", "ManagedIdentityOperator"), role)));
    }
 
    [Fact]
    public async Task ExcludedEnvironment_DoesNotPrepareIdentities()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        builder.AddAzureKubernetesEnvironment("aks").WithAnnotation(ManifestPublishingCallbackAnnotation.Ignore);
 
        await using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        Assert.Empty(model.Resources.OfType<AzureUserAssignedIdentityResource>());
        Assert.Empty(model.Resources.OfType<AzureRoleAssignmentResource>());
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task ClusterScope_IsInheritedByIdentitiesAndOperatorRole(bool configuredScope)
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var scope = configuredScope
            ? new AzureBicepResourceScope(
                builder.AddParameter("cluster-group", "cluster-rg").Resource,
                builder.AddParameter("cluster-subscription", "00000000-0000-0000-0000-000000000001").Resource)
            : new AzureBicepResourceScope("cluster-rg", "00000000-0000-0000-0000-000000000001");
        if (configuredScope)
        {
            aks.ConfigureInfrastructure(infrastructure => infrastructure.AspireResource.Scope = scope);
        }
        else
        {
            aks.Resource.Scope = scope;
        }
 
        await using var app = builder.Build();
        await app.RunAsync();
 
        Assert.Same(scope, aks.Resource.ControlPlaneIdentity!.Scope);
        Assert.Same(scope, aks.Resource.KubeletIdentity!.Scope);
        var operatorRole = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>()
            .Resources.OfType<AzureRoleAssignmentResource>(),
            role => role.TargetAzureResource == aks.Resource.KubeletIdentity);
        Assert.Same(scope, operatorRole.Scope);
        var registryRole = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>()
            .Resources.OfType<AzureRoleAssignmentResource>(),
            role => role.TargetAzureResource == aks.Resource.DefaultContainerRegistry);
        Assert.Null(registryRole.Scope);
 
        var mainBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "main.bicep"));
        var aksBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "aks", "aks.bicep"));
        var operatorBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, operatorRole.Name, $"{operatorRole.Name}.bicep"));
        var registryBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "aks-acr", "aks-acr.bicep"));
        var controlPlaneBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "aks-control-plane-identity", "aks-control-plane-identity.bicep"));
        var kubeletBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "aks-kubelet-identity", "aks-kubelet-identity.bicep"));
        var registryRoleBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, registryRole.Name, $"{registryRole.Name}.bicep"));
        await Verify(mainBicep, "bicep")
            .UseParameters(configuredScope)
            .AppendContentAsFile(aksBicep, "bicep")
            .AppendContentAsFile(operatorBicep, "bicep")
            .AppendContentAsFile(registryBicep, "bicep")
            .AppendContentAsFile(controlPlaneBicep, "bicep")
            .AppendContentAsFile(kubeletBicep, "bicep")
            .AppendContentAsFile(registryRoleBicep, "bicep");
    }
 
    [Fact]
    public async Task IdentityNameCollisionFailsBeforeRegisteringEitherIdentity()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        builder.AddParameter("aks-kubelet-identity");
        builder.AddAzureKubernetesEnvironment("aks");
 
        await using var app = builder.Build();
        var exception = await Assert.ThrowsAsync<DistributedApplicationException>(
            () => ExecuteBeforeStartHooksAsync(app, default));
        Assert.Contains("aks-kubelet-identity", exception.Message);
        Assert.Empty(app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<AzureUserAssignedIdentityResource>());
    }
 
    [Fact]
    public async Task WithContainerRegistry_ExplicitScopeOverridesExistingAnnotationScope()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var registry = builder.AddAzureContainerRegistry("registry").PublishAsExisting("existingacr", "annotation-rg");
        registry.Resource.Scope = new AzureBicepResourceScope("explicit-rg", "00000000-0000-0000-0000-000000000001");
        builder.AddAzureKubernetesEnvironment("aks").WithContainerRegistry(registry);
 
        await using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var roles = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>().Resources
            .OfType<AzureRoleAssignmentResource>(), resource => resource.TargetAzureResource == registry.Resource);
        Assert.Same(registry.Resource.Scope, roles.Scope);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task RegistryAndSubnetCallbacks_ApplyFinalScopeToRoleModules(bool parameterizedScope)
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var registry = builder.AddAzureContainerRegistry("registry").PublishAsExisting("existingacr", "annotation-registry-rg");
        var network = builder.AddAzureVirtualNetwork("network", "10.0.0.0/16")
            .PublishAsExisting("existingvnet", "annotation-network-rg");
        var registryScope = parameterizedScope
            ? new AzureBicepResourceScope(
                builder.AddParameter("registry-group", "registry-rg").Resource,
                builder.AddParameter("registry-subscription", "00000000-0000-0000-0000-000000000001").Resource)
            : new AzureBicepResourceScope("registry-rg", "00000000-0000-0000-0000-000000000001");
        var networkScope = parameterizedScope
            ? new AzureBicepResourceScope(
                builder.AddParameter("network-group", "network-rg").Resource,
                builder.AddParameter("network-subscription", "00000000-0000-0000-0000-000000000002").Resource)
            : new AzureBicepResourceScope("network-rg", "00000000-0000-0000-0000-000000000002");
        registry.ConfigureInfrastructure(infrastructure => infrastructure.AspireResource.Scope = registryScope);
        network.ConfigureInfrastructure(infrastructure => infrastructure.AspireResource.Scope = networkScope);
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithContainerRegistry(registry)
            .WithSubnet(network.AddSubnet("subnet", "10.0.0.0/22"));
 
        await using var app = builder.Build();
        await app.RunAsync();
 
        var roles = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<AzureRoleAssignmentResource>();
        var registryRole = Assert.Single(roles, role => role.TargetAzureResource == registry.Resource);
        var networkRole = Assert.Single(roles, role => role.TargetAzureResource == network.Resource);
        Assert.Same(registryScope, registry.Resource.Scope);
        Assert.Same(registryScope, registryRole.Scope);
        Assert.Same(aks.Resource.KubeletIdentity, registryRole.IdentityResource);
        Assert.Same(networkScope, network.Resource.Scope);
        Assert.Same(networkScope, networkRole.Scope);
        Assert.Same(aks.Resource.ControlPlaneIdentity, networkRole.IdentityResource);
        Assert.Contains(registryRole, aks.Resource.References);
        Assert.Contains(networkRole, aks.Resource.References);
 
        var mainBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "main.bicep"));
        var registryBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "registry", "registry.bicep"));
        var registryRoleBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, registryRole.Name, $"{registryRole.Name}.bicep"));
        var networkBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "network", "network.bicep"));
        var networkRoleBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, networkRole.Name, $"{networkRole.Name}.bicep"));
        var aksBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "aks", "aks.bicep"));
        var controlPlaneBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "aks-control-plane-identity", "aks-control-plane-identity.bicep"));
        var kubeletBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, "aks-kubelet-identity", "aks-kubelet-identity.bicep"));
        var operatorRoleName = "aks-control-plane-identity-roles-aks-kubelet-identity";
        var operatorBicep = await File.ReadAllTextAsync(Path.Combine(workspace.Path, operatorRoleName, $"{operatorRoleName}.bicep"));
        await Verify(mainBicep, "bicep")
            .UseParameters(parameterizedScope)
            .AppendContentAsFile(registryBicep, "bicep")
            .AppendContentAsFile(registryRoleBicep, "bicep")
            .AppendContentAsFile(networkBicep, "bicep")
            .AppendContentAsFile(networkRoleBicep, "bicep")
            .AppendContentAsFile(aksBicep, "bicep")
            .AppendContentAsFile(controlPlaneBicep, "bicep")
            .AppendContentAsFile(kubeletBicep, "bicep")
            .AppendContentAsFile(operatorBicep, "bicep");
    }
 
    [Fact]
    public async Task WithSubnet_GrantsControlPlaneNetworkAccessForFinalEnvironmentAndPoolSubnets()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
        var firstNetwork = builder.AddAzureVirtualNetwork("first-network", "10.0.0.0/16");
        var finalNetwork = builder.AddAzureVirtualNetwork("final-network", "10.1.0.0/16");
        var poolNetwork = builder.AddAzureVirtualNetwork("pool-network", "10.2.0.0/16")
            .PublishAsExisting("pool-vnet", "network-rg");
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSubnet(firstNetwork.AddSubnet("first-subnet", "10.0.0.0/22"))
            .WithSubnet(finalNetwork.AddSubnet("final-subnet", "10.1.0.0/22"));
        aks.AddNodePool("workload", "Standard_D2s_v5")
            .WithSubnet(poolNetwork.AddSubnet("pool-subnet", "10.2.0.0/22"));
        aks.AddNodePool("second", "Standard_D2s_v5")
            .WithSubnet(poolNetwork.AddSubnet("second-subnet", "10.2.4.0/22"));
 
        await using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var roles = app.Services.GetRequiredService<DistributedApplicationModel>().Resources
            .OfType<AzureRoleAssignmentResource>().Where(role => role.TargetAzureResource is AzureVirtualNetworkResource);
        Assert.Collection(roles,
            role =>
            {
                Assert.Same(aks.Resource.ControlPlaneIdentity, role.IdentityResource);
                Assert.Same(poolNetwork.Resource, role.TargetAzureResource);
                Assert.Equal("network-rg", role.Scope!.ResourceGroup);
                Assert.Contains(role, aks.Resource.References);
            },
            role =>
            {
                Assert.Same(aks.Resource.ControlPlaneIdentity, role.IdentityResource);
                Assert.Same(finalNetwork.Resource, role.TargetAzureResource);
                Assert.Null(role.Scope);
                Assert.Contains(role, aks.Resource.References);
            });
        foreach (var annotation in aks.Resource.ControlPlaneIdentity!.Annotations.OfType<RoleAssignmentAnnotation>()
            .Where(annotation => annotation.Target is AzureVirtualNetworkResource))
        {
            Assert.Collection(annotation.Roles,
                role => Assert.Equal(new RoleDefinition("4d97b98b-1d4f-4787-a291-c67834d212e7", "NetworkContributor"), role));
        }
    }
 
    [Fact]
    public void WithSystemNodePool_CustomVmSize()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool("Standard_B2s");
 
        Assert.Single(aks.Resource.NodePools, p => p.Mode is AksNodePoolMode.System);
        var systemPool = aks.Resource.NodePools.First(p => p.Mode is AksNodePoolMode.System);
        Assert.Equal("system", systemPool.Name);
        Assert.Equal("Standard_B2s", systemPool.VmSize);
        Assert.Equal(1, systemPool.MinCount);
        Assert.Equal(3, systemPool.MaxCount);
    }
 
    [Fact]
    public void WithSystemNodePool_CustomVmSizeAndScaling()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool("Standard_B4ms", minCount: 2, maxCount: 5);
 
        var systemPool = aks.Resource.NodePools.First(p => p.Mode is AksNodePoolMode.System);
        Assert.Equal("Standard_B4ms", systemPool.VmSize);
        Assert.Equal(2, systemPool.MinCount);
        Assert.Equal(5, systemPool.MaxCount);
    }
 
    [Fact]
    public void WithSystemNodePool_ReplacesDefaultSystemPool()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
 
        // Default system pool should exist
        Assert.Equal("Standard_D2s_v5", aks.Resource.NodePools[0].VmSize);
 
        // Replace it
        aks.WithSystemNodePool("Standard_B2s");
 
        // Should still be exactly one system pool
        Assert.Single(aks.Resource.NodePools, p => p.Mode is AksNodePoolMode.System);
        Assert.Equal("Standard_B2s", aks.Resource.NodePools.First(p => p.Mode is AksNodePoolMode.System).VmSize);
    }
 
    [Fact]
    public void WithSystemNodePool_CalledMultipleTimesUsesLastValue()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool("Standard_B2s")
            .WithSystemNodePool("Standard_D4s_v5", minCount: 3, maxCount: 10);
 
        Assert.Single(aks.Resource.NodePools, p => p.Mode is AksNodePoolMode.System);
        var systemPool = aks.Resource.NodePools.First(p => p.Mode is AksNodePoolMode.System);
        Assert.Equal("Standard_D4s_v5", systemPool.VmSize);
        Assert.Equal(3, systemPool.MinCount);
        Assert.Equal(10, systemPool.MaxCount);
    }
 
    [Fact]
    public void WithSystemNodePool_ChainsWithAddNodePool()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool("Standard_B2s");
 
        var gpuPool = aks.AddNodePool("gpu", "Standard_NC6s_v3", 0, 5);
 
        // 1 system pool + 1 user pool
        Assert.Equal(2, aks.Resource.NodePools.Count);
        Assert.Equal("Standard_B2s", aks.Resource.NodePools.First(p => p.Mode is AksNodePoolMode.System).VmSize);
        Assert.Equal("gpu", gpuPool.Resource.Name);
    }
 
    [Fact]
    public void WithSystemNodePool_RejectsZeroMinCount()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        Assert.Throws<ArgumentOutOfRangeException>(() => aks.WithSystemNodePool("Standard_B2s", minCount: 0));
    }
 
    [Fact]
    public async Task WithSystemNodePool_BicepReflectsCustomVmSize()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(
            outputHelper,
            workspace,
            operation: DistributedApplicationOperation.Run);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks")
            .WithSystemNodePool("Standard_B2s");
 
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        await Verify(manifest.BicepText, extension: "bicep");
    }
 
    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
    private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(true, false)]
    [InlineData(false, true)]
    [InlineData(true, true)]
    public void RequiresPreviewIngressApi_DependsOnApplicationLoadBalancer(bool gatewayApiEnabled, bool applicationLoadBalancerEnabled)
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        aks.Resource.GatewayApiEnabled = gatewayApiEnabled;
        aks.Resource.ApplicationLoadBalancerEnabled = applicationLoadBalancerEnabled;
 
        Assert.Equal(applicationLoadBalancerEnabled, aks.Resource.RequiresPreviewIngressApi);
    }
 
    [Fact]
    public async Task GatewayApiEnabled_BicepUsesStableApi()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        aks.Resource.GatewayApiEnabled = true;
 
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        Assert.Contains("resource aks 'Microsoft.ContainerService/managedClusters@2026-07-01' = {", manifest.BicepText);
        Assert.Contains("installation: 'Standard'", manifest.BicepText);
    }
 
    [Fact]
    public async Task AddLoadBalancer_BicepEnablesIngressProfileAndUsesPreviewApi()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var aksSubnet = vnet.AddSubnet("aksnodes", "10.0.0.0/22");
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks").WithSubnet(aksSubnet);
        aks.AddLoadBalancer("lb", albSubnet);
 
        var manifest = await AzureManifestUtils.GetManifestWithBicep(aks.Resource);
        await Verify(manifest.BicepText, extension: "bicep");
    }
 
    [Fact]
    public void AddLoadBalancer_AppliesSubnetDelegation()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        aks.AddLoadBalancer("lb", albSubnet);
 
        Assert.True(aks.Resource.GatewayApiEnabled);
        Assert.True(aks.Resource.ApplicationLoadBalancerEnabled);
        Assert.True(aks.Resource.RequiresPreviewIngressApi);
 
        // AGC requires the subnet be delegated to Microsoft.ServiceNetworking/trafficControllers.
        Assert.True(albSubnet.Resource.TryGetLastAnnotation<AzureSubnetServiceDelegationAnnotation>(out var delegation));
        Assert.Equal("Microsoft.ServiceNetworking/trafficControllers", delegation!.ServiceName);
    }
 
    [Fact]
    public void AddLoadBalancer_RegistersPerLBPipelineStep()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb = aks.AddLoadBalancer("lb", albSubnet);
 
        Assert.Same(aks.Resource, lb.Resource.Parent);
        Assert.Equal("alb-lb", lb.Resource.AlbName);
        Assert.True(lb.Resource.TryGetAnnotationsOfType<PipelineStepAnnotation>(out var stepAnnotations));
        Assert.Single(stepAnnotations);
    }
 
    [Fact]
    public void AddLoadBalancer_MultipleLBs_AllStepsRegistered()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var alb1 = vnet.AddSubnet("alb1", "10.0.4.0/24");
        var alb2 = vnet.AddSubnet("alb2", "10.0.5.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb1 = aks.AddLoadBalancer("lb1", alb1);
        var lb2 = aks.AddLoadBalancer("lb2", alb2);
 
        Assert.NotSame(lb1.Resource, lb2.Resource);
        Assert.Equal("alb-lb1", lb1.Resource.AlbName);
        Assert.Equal("alb-lb2", lb2.Resource.AlbName);
 
        // Each LB owns its own pipeline step annotation.
        Assert.True(lb1.Resource.TryGetAnnotationsOfType<PipelineStepAnnotation>(out var lb1Steps));
        Assert.True(lb2.Resource.TryGetAnnotationsOfType<PipelineStepAnnotation>(out var lb2Steps));
        Assert.Single(lb1Steps);
        Assert.Single(lb2Steps);
 
        // Both LBs share the subnet/delegation requirements but the annotation
        // is applied idempotently per subnet (one delegation per subnet).
        Assert.True(alb1.Resource.TryGetLastAnnotation<AzureSubnetServiceDelegationAnnotation>(out _));
        Assert.True(alb2.Resource.TryGetLastAnnotation<AzureSubnetServiceDelegationAnnotation>(out _));
    }
 
    [Fact]
    public void AddLoadBalancer_OnUserDelegatedSubnet_ReplacesDelegationAndRecordsDisplaced()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
 
        // The user delegates their ALB subnet to a different service before handing it to AGC.
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24")
            .WithServiceDelegation("Microsoft.Netapp/volumes");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb = aks.AddLoadBalancer("lb", albSubnet);
 
        // AGC requires trafficControllers, so the user's delegation is replaced rather than appended
        // and the subnet retains exactly one effective delegation.
        var delegation = Assert.Single(albSubnet.Resource.Annotations.OfType<AzureSubnetServiceDelegationAnnotation>());
        Assert.Equal("Microsoft.ServiceNetworking/trafficControllers", delegation.ServiceName);
 
        // The displaced delegation is surfaced so the LB pipeline step can warn the user at deploy time.
        Assert.Equal("Microsoft.Netapp/volumes", lb.Resource.DisplacedDelegationServiceName);
    }
 
    [Fact]
    public void AddLoadBalancer_SharedSubnet_KeepsSingleDelegationWithNoDisplacement()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb1 = aks.AddLoadBalancer("lb1", albSubnet);
        var lb2 = aks.AddLoadBalancer("lb2", albSubnet);
 
        // Sharing a subnet across load balancers is idempotent: exactly one delegation remains.
        var delegation = Assert.Single(albSubnet.Resource.Annotations.OfType<AzureSubnetServiceDelegationAnnotation>());
        Assert.Equal("Microsoft.ServiceNetworking/trafficControllers", delegation.ServiceName);
 
        // Neither LB displaced a user delegation; the only prior delegation was AGC's own.
        Assert.Null(lb1.Resource.DisplacedDelegationServiceName);
        Assert.Null(lb2.Resource.DisplacedDelegationServiceName);
    }
 
    [Fact]
    public void AddLoadBalancer_EquivalentDelegationWithDifferentCasing_DoesNotRecordDisplacement()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24")
            .WithServiceDelegation("microsoft.servicenetworking/trafficcontrollers");
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb = aks.AddLoadBalancer("lb", albSubnet);
 
        var delegation = Assert.Single(albSubnet.Resource.Annotations.OfType<AzureSubnetServiceDelegationAnnotation>());
        Assert.Equal("Microsoft.ServiceNetworking/trafficControllers", delegation.ServiceName);
        Assert.Null(lb.Resource.DisplacedDelegationServiceName);
    }
 
    [Fact]
    public void AddLoadBalancer_OnSubnetWithMultipleDirectDelegations_CollapsesToSingle()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
 
        var vnet = builder.AddAzureVirtualNetwork("vnet", "10.0.0.0/16");
 
        // AzureSubnetServiceDelegationAnnotation is public, so a subnet can carry several directly-appended
        // delegations. AddLoadBalancer must read the last one (last-write-wins) without throwing on the
        // duplicates and then collapse them to AGC's single required delegation.
        var albSubnet = vnet.AddSubnet("alb", "10.0.4.0/24")
            .WithAnnotation(new AzureSubnetServiceDelegationAnnotation("First", "Microsoft.Netapp/volumes"))
            .WithAnnotation(new AzureSubnetServiceDelegationAnnotation("Second", "Microsoft.Sql/managedInstances"));
 
        var aks = builder.AddAzureKubernetesEnvironment("aks");
        var lb = aks.AddLoadBalancer("lb", albSubnet);
 
        var delegation = Assert.Single(albSubnet.Resource.Annotations.OfType<AzureSubnetServiceDelegationAnnotation>());
        Assert.Equal("Microsoft.ServiceNetworking/trafficControllers", delegation.ServiceName);
 
        // The last of the user's delegations is surfaced as displaced so the LB pipeline step can warn.
        Assert.Equal("Microsoft.Sql/managedInstances", lb.Resource.DisplacedDelegationServiceName);
    }
 
}