File: KubernetesDeployWithHelmChartTests.cs
Web Access
Project: src\tests\Aspire.Cli.EndToEnd.Tests\Aspire.Cli.EndToEnd.Tests.csproj (Aspire.Cli.EndToEnd.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.EndToEnd.Tests.Helpers;
using Hex1b.Automation;
using Xunit;
 
namespace Aspire.Cli.EndToEnd.Tests;
 
/// <summary>
/// E2E test for <c>aspire deploy</c> to Kubernetes with an external Helm chart
/// installed via <c>AddHelmChart</c>. Verifies that both the Aspire application
/// and the external podinfo chart are deployed successfully.
/// </summary>
public sealed class KubernetesDeployWithHelmChartTests(ITestOutputHelper output)
{
    private const string ProjectName = "K8sHelmChartTest";
 
    [Fact]
    [CaptureWorkspaceOnFailure]
    public async Task DeployK8sWithExternalHelmChart()
    {
        var repoRoot = CliE2ETestHelpers.GetRepoRoot();
        var strategy = CliInstallStrategy.Detect(output.WriteLine);
        using var workspace = TemporaryWorkspace.Create(output);
 
        var clusterName = KubernetesDeployTestHelpers.GenerateUniqueClusterName();
        var k8sNamespace = $"test-{clusterName[..16]}";
 
        output.WriteLine($"Cluster name: {clusterName}");
        output.WriteLine($"Namespace: {k8sNamespace}");
 
        using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
        var counter = new SequenceCounter();
        var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
        await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken);
 
        // Prepare environment
        await auto.PrepareDockerEnvironmentAsync(counter, workspace);
        await auto.InstallAspireCliAsync(strategy, counter);
 
        await auto.VerifyPullRequestCliVersionAsync(counter);
 
        try
        {
            // =====================================================================
            // Phase 1: Install KinD + Helm, create cluster with local registry
            // =====================================================================
 
            await auto.InstallKindAndHelmAsync(counter);
            await auto.CreateKindClusterWithRegistryAsync(counter, clusterName);
 
            // =====================================================================
            // Phase 2: Scaffold the project with AddHelmChart for podinfo
            // =====================================================================
 
            var appHostCode = $$"""
                #pragma warning disable ASPIRECOMPUTE003
                using Aspire.Hosting;
                using Aspire.Hosting.Kubernetes;
 
                var builder = DistributedApplication.CreateBuilder(args);
 
                var registryEndpoint = builder.AddParameter("registryendpoint");
                var registry = builder.AddContainerRegistry("registry", registryEndpoint);
 
                var api = builder.AddProject<Projects.{{ProjectName}}_ApiService>("server")
                    .WithExternalHttpEndpoints();
 
                var k8s = builder.AddKubernetesEnvironment("env")
                    .WithHelm(helm =>
                    {
                        helm.WithNamespace(builder.AddParameter("namespace"));
                        helm.WithChartVersion(builder.AddParameter("chartversion"));
                    });
 
                // Install an external Helm chart (podinfo) alongside the app
                k8s.AddHelmChart("podinfo", "oci://ghcr.io/stefanprodan/charts/podinfo", "6.7.1")
                    .WithHelmValue("replicaCount", "2")
                    .WithHelmValue("ui.message", "Aspire AddHelmChart works!");
 
                builder.Build().Run();
                """;
 
            var apiProgramCode = """
                var builder = WebApplication.CreateBuilder(args);
                builder.AddServiceDefaults();
 
                var app = builder.Build();
                app.MapDefaultEndpoints();
 
                app.MapGet("/test-deployment", () =>
                {
                    return Results.Ok("PASSED: API service with external Helm chart");
                });
 
                app.Run();
                """;
 
            await auto.ScaffoldK8sDeployProjectAsync(
                counter,
                ProjectName,
                Path.Combine(workspace.WorkspaceRoot.FullName, ProjectName),
                appHostHostingPackages: ["Aspire.Hosting.Kubernetes"],
                apiClientPackages: [],
                appHostCode: appHostCode,
                apiProgramCode: apiProgramCode,
                output: output);
 
            // =====================================================================
            // Phase 3: Run aspire deploy interactively
            // =====================================================================
 
            await auto.AspireDeployInteractiveAsync(
                counter,
                parameterResponses:
                [
                    ("registryendpoint", "localhost:5001"),
                    ("namespace", k8sNamespace),
                    ("chartversion", "0.1.0"),
                ]);
 
            // =====================================================================
            // Phase 4: Verify the Aspire app deployment
            // =====================================================================
 
            await auto.VerifyDeploymentAsync(
                counter,
                @namespace: k8sNamespace,
                serviceName: "server",
                localPort: 18080,
                testPath: "/test-deployment");
 
            // =====================================================================
            // Phase 5: Verify the external Helm chart (podinfo) was installed
            // =====================================================================
 
            // Check that podinfo pods are running in its own namespace
            await auto.TypeAsync("kubectl get pods -n podinfo -l app.kubernetes.io/name=podinfo");
            await auto.EnterAsync();
            await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30));
 
            // Verify podinfo has 2 replicas as configured
            await auto.TypeAsync(
                "READY=$(kubectl get deploy -n podinfo -l app.kubernetes.io/name=podinfo " +
                "-o jsonpath='{.items[0].status.readyReplicas}') && " +
                "[ \"$READY\" = \"2\" ] && echo 'VERIFY_OK: podinfo has 2 replicas' || " +
                "echo \"FAIL: expected 2 replicas, got $READY\"");
            await auto.EnterAsync();
            await auto.WaitUntilTextAsync("VERIFY_OK", timeout: TimeSpan.FromSeconds(60));
            await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(10));
 
            // Verify the Helm release exists
            await auto.TypeAsync("helm list -n podinfo");
            await auto.EnterAsync();
            await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30));
 
            // =====================================================================
            // Phase 6: Cleanup
            // =====================================================================
 
            await auto.CleanupKubernetesDeploymentAsync(counter, clusterName);
        }
        finally
        {
            await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output);
        }
    }
}