File: KubernetesEnvironmentResourceTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Kubernetes.Tests\Aspire.Hosting.Kubernetes.Tests.csproj (Aspire.Hosting.Kubernetes.Tests)
#pragma warning disable ASPIRECOMPUTE002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Runtime.CompilerServices;
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Kubernetes;
using Aspire.Hosting.Utils;
using Aspire.TestUtilities;
using Microsoft.Extensions.DependencyInjection;
 
public class KubernetesEnvironmentResourceTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task PublishingKubernetesEnvironmentPublishesFile()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, workspace.Path);
 
        builder.AddKubernetesEnvironment("env");
 
        // Add a container to the application
        builder.AddContainer("service", "nginx");
 
        var app = builder.Build();
        app.Run();
 
        var chartYaml = Path.Combine(workspace.Path, "Chart.yaml");
        var valuesYaml = Path.Combine(workspace.Path, "values.yaml");
        var deploymentYaml = Path.Combine(workspace.Path, "templates", "service", "deployment.yaml");
 
        await Verify(File.ReadAllText(chartYaml), "yaml")
            .AppendContentAsFile(File.ReadAllText(valuesYaml), "yaml")
            .AppendContentAsFile(File.ReadAllText(deploymentYaml), "yaml");
    }
 
    [Fact]
    [ActiveIssue("https://github.com/microsoft/aspire/issues/11818", typeof(PlatformDetection), nameof(PlatformDetection.IsRunningFromAzdo))]
    public async Task PublishAsKubernetesService_ThrowsIfNoEnvironment()
    {
        static async Task RunTest(Action<IDistributedApplicationBuilder> action)
        {
            var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
            // Do not add AddKubernetesEnvironment
 
            action(builder);
 
            using var app = builder.Build();
 
            var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => ExecuteBeforeStartHooksAsync(app, default));
 
            Assert.Contains("there are no 'KubernetesEnvironmentResource' resources", ex.Message);
        }
 
        await RunTest(builder =>
            builder.AddProject<Projects.ServiceA>("ServiceA")
                .PublishAsKubernetesService((_) => { }));
 
        await RunTest(builder =>
            builder.AddContainer("api", "myimage")
                .PublishAsKubernetesService((_) => { }));
 
        await RunTest(builder =>
            builder.AddExecutable("exe", "path/to/executable", ".")
                .PublishAsDockerFile()
                .PublishAsKubernetesService((_) => { }));
    }
 
    [Fact]
    public async Task ValidateKubernetes_DoesNotThrowInRunMode()
    {
        // Regression test for https://github.com/microsoft/aspire/issues/16940.
        // In run mode, AddKubernetesEnvironment does not add the env resource to the model.
        // If a compute resource still ends up with a KubernetesServiceCustomizationAnnotation
        // (e.g. via WithAnnotation), the validation step should not throw at 'aspire run' time —
        // PublishAs* customizations are only meaningful at publish/deploy time.
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        builder.AddKubernetesEnvironment("env");
 
        builder.AddContainer("api", "myimage")
            .WithAnnotation(new KubernetesServiceCustomizationAnnotation((_) => { }));
 
        using var app = builder.Build();
 
        await ExecuteBeforeStartHooksAsync(app, default);
    }
 
    [Fact]
    [ActiveIssue("https://github.com/microsoft/aspire/issues/11818", typeof(PlatformDetection), nameof(PlatformDetection.IsRunningFromAzdo))]
    public async Task MultipleKubernetesEnvironmentsSupported()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var outputPath = Path.Combine(workspace.Path, "output");
 
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath);
 
        var env1 = builder.AddKubernetesEnvironment("env1");
        var env2 = builder.AddKubernetesEnvironment("env2");
 
        builder.AddProject<Projects.ServiceA>("ServiceA")
            .WithComputeEnvironment(env1);
 
        builder.AddProject<Projects.ServiceB>("ServiceB")
            .WithComputeEnvironment(env2);
 
        using var app = builder.Build();
 
        // Publishing will stop the app when it is done
        await app.RunAsync();
 
        await VerifyDirectory(outputPath);
    }
 
    [Fact]
    public async Task GetHostAddressExpression()
    {
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var env = builder.AddKubernetesEnvironment("env");
 
        var project = builder
            .AddProject<Projects.ServiceA>("project1", launchProfileName: null)
            .WithHttpEndpoint();
 
        var endpointReferenceEx = env.Resource.GetHostAddressExpression(project.GetEndpoint("http"));
        Assert.NotNull(endpointReferenceEx);
 
        Assert.Equal("project1-service", endpointReferenceEx.Format);
        Assert.Empty(endpointReferenceEx.ValueProviders);
    }
 
    [Fact]
    public async Task MultipleComputeEnvironmentsOnlyProcessTargetedResources()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var kubernetes = builder.AddKubernetesEnvironment("kubernetes");
        var dockerCompose = builder.AddDockerComposeEnvironment("docker-compose");
 
        // Container targeted to Kubernetes
        var containerForK8s = builder.AddContainer("containerk8s", "nginx")
            .WithHttpEndpoint(port: 8080, targetPort: 80, name: "http")
            .WithComputeEnvironment(kubernetes);
 
        // Container targeted to Docker Compose
        var containerForDocker = builder.AddContainer("containerdocker", "nginx")
            .WithHttpEndpoint(port: 9090, targetPort: 80, name: "http")
            .WithComputeEnvironment(dockerCompose);
 
        // Project targeted to Kubernetes
        var projectForK8s = builder.AddProject<Projects.ServiceA>("projectk8s", launchProfileName: null)
            .WithHttpEndpoint()
            .WithComputeEnvironment(kubernetes);
 
        // Project targeted to Docker Compose
        var projectForDocker = builder.AddProject<Projects.ServiceA>("projectdocker", launchProfileName: null)
            .WithHttpEndpoint()
            .WithComputeEnvironment(dockerCompose);
 
        using var app = builder.Build();
 
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify containerForK8s has a deployment target for Kubernetes
        var containerK8sResource = model.Resources.First(r => r.Name == "containerk8s");
        var containerK8sTarget = containerK8sResource.GetDeploymentTargetAnnotation(kubernetes.Resource);
        Assert.NotNull(containerK8sTarget);
        Assert.Same(kubernetes.Resource, containerK8sTarget.ComputeEnvironment);
 
        // Verify containerForDocker has a deployment target for Docker Compose
        var containerDockerResource = model.Resources.First(r => r.Name == "containerdocker");
        var containerDockerTarget = containerDockerResource.GetDeploymentTargetAnnotation(dockerCompose.Resource);
        Assert.NotNull(containerDockerTarget);
        Assert.Same(dockerCompose.Resource, containerDockerTarget.ComputeEnvironment);
 
        // Verify projectForK8s has a deployment target for Kubernetes
        var projectK8sResource = model.Resources.First(r => r.Name == "projectk8s");
        var projectK8sTarget = projectK8sResource.GetDeploymentTargetAnnotation(kubernetes.Resource);
        Assert.NotNull(projectK8sTarget);
        Assert.Same(kubernetes.Resource, projectK8sTarget.ComputeEnvironment);
 
        // Verify projectForDocker has a deployment target for Docker Compose
        var projectDockerResource = model.Resources.First(r => r.Name == "projectdocker");
        var projectDockerTarget = projectDockerResource.GetDeploymentTargetAnnotation(dockerCompose.Resource);
        Assert.NotNull(projectDockerTarget);
        Assert.Same(dockerCompose.Resource, projectDockerTarget.ComputeEnvironment);
 
        // Verify resources do NOT have deployment targets for other environments
        Assert.Null(containerK8sResource.GetDeploymentTargetAnnotation(dockerCompose.Resource));
        Assert.Null(containerDockerResource.GetDeploymentTargetAnnotation(kubernetes.Resource));
        Assert.Null(projectK8sResource.GetDeploymentTargetAnnotation(dockerCompose.Resource));
        Assert.Null(projectDockerResource.GetDeploymentTargetAnnotation(kubernetes.Resource));
    }
 
    [Fact]
    public async Task KubernetesPersistentVolumeCannotTargetDockerCompose()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var kubernetes = builder.AddKubernetesEnvironment("kubernetes");
        var dockerCompose = builder.AddDockerComposeEnvironment("docker-compose");
        var volume = kubernetes.AddPersistentVolume("data");
 
        builder.AddProject<Projects.ServiceA>("project", launchProfileName: null)
            .WithComputeEnvironment(dockerCompose)
            .WithPersistentVolume(volume, "/srv/data", env: "DATA_PATH");
 
        using var app = builder.Build();
        var exception = await Assert.ThrowsAsync<DistributedApplicationException>(
            () => ExecuteBeforeStartHooksAsync(app, CancellationToken.None));
 
        Assert.Contains("project", exception.Message);
        Assert.Contains("docker-compose", exception.Message);
        Assert.Contains("data", exception.Message);
        Assert.Contains("kubernetes", exception.Message);
    }
 
    [Fact]
    public async Task RunningTheDeploymentTargetHooksTwiceLeavesOneTargetPerEnvironment()
    {
        // The prepare-deployment-target step declares RequiredBySteps = [BeforeStart], so it runs once in
        // the BeforeStart pipeline and again in the publish DAG. Without a guard the second pass adds a
        // second DeploymentTargetAnnotation, and GetDeploymentTargetAnnotation then throws on the
        // ambiguity, which fails every publish that goes through both paths.
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var kubernetes = builder.AddKubernetesEnvironment("kubernetes");
        var dockerCompose = builder.AddDockerComposeEnvironment("docker-compose");
 
        builder.AddContainer("containerk8s", "nginx")
            .WithHttpEndpoint(port: 8080, targetPort: 80, name: "http")
            .WithComputeEnvironment(kubernetes);
 
        builder.AddContainer("containerdocker", "nginx")
            .WithHttpEndpoint(port: 9090, targetPort: 80, name: "http")
            .WithComputeEnvironment(dockerCompose);
 
        using var app = builder.Build();
 
        await ExecuteBeforeStartHooksAsync(app, default);
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var containerK8sResource = model.Resources.First(r => r.Name == "containerk8s");
        var containerDockerResource = model.Resources.First(r => r.Name == "containerdocker");
 
        Assert.Single(containerK8sResource.Annotations.OfType<DeploymentTargetAnnotation>());
        Assert.Single(containerDockerResource.Annotations.OfType<DeploymentTargetAnnotation>());
 
        Assert.Same(kubernetes.Resource, containerK8sResource.GetDeploymentTargetAnnotation()!.ComputeEnvironment);
        Assert.Same(dockerCompose.Resource, containerDockerResource.GetDeploymentTargetAnnotation()!.ComputeEnvironment);
 
        // The second pass must not have produced a fresh, unconfigured target either: the Kubernetes
        // service still has to carry the endpoint that was mapped on the first pass.
        var service = Assert.IsType<KubernetesResource>(containerK8sResource.GetDeploymentTargetAnnotation()!.DeploymentTarget);
        var mapping = Assert.Contains("http", service.EndpointMappings);
        Assert.Equal("http", mapping.Scheme);
    }
 
    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
    private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);
}