File: GoKubernetesDeployTests.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 a Go AppHost created from the starter template.
/// Creates a Go API + Redis project, adds Kubernetes support, and deploys to a KinD cluster.
/// </summary>
public sealed class GoKubernetesDeployTests(ITestOutputHelper output)
{
    private const string ProjectName = "K8sGoTest";
 
    [Fact]
    [CaptureWorkspaceOnFailure]
    public async Task DeployGoAppToKubernetes()
    {
        var repoRoot = CliE2ETestHelpers.GetRepoRoot();
        var strategy = CliInstallStrategy.Detect(output.WriteLine);
        using var workspace = TemporaryWorkspace.Create(output);
 
        var clusterName = KubernetesDeployTestHelpers.GenerateUniqueClusterName();
        const string k8sNamespace = "default";
 
        output.WriteLine($"Cluster name: {clusterName}");
        output.WriteLine($"Namespace: {k8sNamespace}");
 
        using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.PolyglotGo, 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);
 
        await auto.PrepareDockerEnvironmentAsync(counter, workspace);
        await auto.InstallAspireCliAsync(strategy, counter);
 
        await auto.VerifyPullRequestCliVersionAsync(counter);
        await auto.EnableExperimentalGoSupportAsync(counter);
 
        try
        {
            // =====================================================================
            // Phase 1: Install KinD + Helm, create cluster with local registry
            // =====================================================================
 
            await auto.InstallKindAndHelmAsync(counter);
            await auto.CreateKindClusterWithRegistryAsync(counter, clusterName);
 
            // =====================================================================
            // Phase 2: Create Go project from template
            // =====================================================================
 
            await auto.AspireNewAsync(ProjectName, counter, template: AspireTemplate.GoStarter);
 
            await auto.TypeAsync($"cd {ProjectName}");
            await auto.EnterAsync();
            await auto.WaitForSuccessPromptAsync(counter);
 
            // Add Kubernetes hosting package
            await auto.AspireAddPackageAsync("Aspire.Hosting.Kubernetes", counter);
 
            // Regenerate the Go SDK with Kubernetes types
            await auto.RunCommandAsync("aspire restore --non-interactive", counter, TimeSpan.FromMinutes(3));
 
            // The starter ships api/go.mod without a go.sum, and its "go mod tidy" resource only runs
            // under "aspire run". The generated Dockerfile builds from go.mod and go.sum alone, so
            // the container build needs the go.sum that "go mod tidy" writes.
            await auto.RunCommandAsync("(cd api && go mod tidy)", counter, TimeSpan.FromMinutes(3));
 
            // =====================================================================
            // Phase 3: Modify apphost.go to add Kubernetes environment
            // =====================================================================
 
            var appHostFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, ProjectName, "apphost.go");
 
            output.WriteLine($"Looking for apphost.go at: {appHostFilePath}");
 
            // The AppHost passes no WithHelm callback: a Configure func inside the generated Go options
            // struct panics the options merge (https://github.com/microsoft/aspire/issues/18080), so the
            // Helm defaults apply (namespace "default", chart version 0.1.0).
            GoAppHostTestHelpers.InsertBeforeBuild(
                appHostFilePath,
                """
                // Add container registry for image push
                registryEndpoint := builder.AddParameter("registryendpoint")
                builder.AddContainerRegistry("registry", registryEndpoint)
 
                // Add Kubernetes environment (Helm is the default deployment engine)
                builder.AddKubernetesEnvironment("env")
                """);
            output.WriteLine("Modified apphost.go with Kubernetes environment configuration");
 
            // =====================================================================
            // Phase 4: Run aspire deploy interactively
            // =====================================================================
 
            await auto.AspireDeployInteractiveAsync(
                counter,
                parameterResponses:
                [
                    ("registryendpoint", "localhost:5001"),
                ]);
 
            // =====================================================================
            // Phase 5: Verify the deployment
            // =====================================================================
 
            // The publisher labels each workload and its pods with app.kubernetes.io/component=<AppHost resource name>.
            // Selecting by that label makes a chart without the workload fail the test: "kubectl wait" exits 1 with
            // "no matching resources found" when nothing matches, whereas "kubectl get pods" succeeds on an empty
            // namespace and Helm's --wait only waits for workloads that are in the chart.
            foreach (var resourceName in new[] { "api", "cache" })
            {
                await auto.RunCommandAsync(
                    $"kubectl wait --for=condition=Ready pod -n {k8sNamespace} -l app.kubernetes.io/component={resourceName} --timeout=180s",
                    counter,
                    TimeSpan.FromMinutes(4));
            }
 
            // =====================================================================
            // Phase 6: Cleanup
            // =====================================================================
 
            await auto.CleanupKubernetesDeploymentAsync(counter, clusterName);
        }
        finally
        {
            await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output);
        }
    }
}