File: KubernetesDeployWithFileKeyRefTests.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;
 
public sealed class KubernetesDeployWithFileKeyRefTests(ITestOutputHelper output)
{
    // EnvFiles is enabled by default in 1.35. Pin the node image independently of
    // KinD's default so the scenario continues to exercise a known feature version.
    private const string NodeImage = "kindest/node:v1.35.0@sha256:452d707d4862f52530247495d180205e029056831160e22870e37e3f6c1ac31f";
    private const string ProjectName = "K8sFileKeyRefTest";
 
    [Fact]
    [CaptureWorkspaceOnFailure]
    public async Task DeployK8sWithFileKeyRef()
    {
        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]}";
        var serverName = $"server-{clusterName}";
        var imageName = $"{clusterName}/server";
        var expectedImage = $"localhost:5001/{imageName}:filekeyref";
        var expectedValue = $"generated-{clusterName}";
        var response = $"PASSED: {expectedValue}|second value|optional-unset";
 
        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);
 
        await auto.PrepareDockerEnvironmentAsync(counter, workspace);
        await auto.InstallAspireCliAsync(strategy, counter);
        await auto.VerifyPullRequestCliVersionAsync(counter);
 
        try
        {
            await auto.InstallKindAndHelmAsync(counter);
            await auto.CreateKindClusterWithRegistryAsync(counter, clusterName, NodeImage);
 
            var appHostCode = $$"""
                #pragma warning disable ASPIRECOMPUTE003
                #pragma warning disable ASPIREPIPELINES003
                using Aspire.Hosting;
                using Aspire.Hosting.Kubernetes;
                using Aspire.Hosting.Kubernetes.Resources;
 
                var builder = DistributedApplication.CreateBuilder(args);
                var registry = builder.AddContainerRegistry("registry", builder.AddParameter("registryendpoint"));
 
                builder.AddDotnetProject("{{serverName}}", "../{{ProjectName}}.ApiService/{{ProjectName}}.ApiService.csproj")
                    .WithRemoteImageName("{{imageName}}")
                    .WithRemoteImageTag("filekeyref")
                    .WithExternalHttpEndpoints()
                    .PublishAsKubernetesService(resource =>
                    {
                        var pod = resource.Workload!.PodTemplate.Spec;
                        pod.Volumes.Add(new VolumeV1 { Name = "generated-env", EmptyDir = new() });
                        pod.InitContainers.Add(new ContainerV1
                        {
                            Name = "prepare-env",
                            Image = "docker.io/library/busybox:1.37@sha256:bdf57e528e45e4433820e045b29b4597825a1c9e38353532d90a01445013f82e",
                            Command = { "sh", "-ec" },
                            Args = { "printf \"SOURCE_KEY='{{expectedValue}}'\\nSECOND_KEY='second value'\\n\" > /out/app.env" },
                            VolumeMounts = { new VolumeMountV1 { Name = "generated-env", MountPath = "/out" } }
                        });
                        foreach (var (name, key, optional) in new[]
                        {
                            ("APP_VALUE", "SOURCE_KEY", false),
                            ("SECOND_VALUE", "SECOND_KEY", false),
                            ("OPTIONAL_VALUE", "MISSING_KEY", true)
                        })
                        {
                            pod.Containers[0].Env.Add(new EnvVarV1
                            {
                                Name = name,
                                ValueFrom = new EnvVarSourceV1
                                {
                                    FileKeyRef = new FileKeySelectorV1
                                    {
                                        VolumeName = "generated-env",
                                        Path = "app.env",
                                        Key = key,
                                        Optional = optional
                                    }
                                }
                            });
                        }
                    });
 
                builder.AddKubernetesEnvironment("env")
                    .WithDashboard(false)
                    .WithHelm(helm =>
                    {
                        helm.WithNamespace(builder.AddParameter("namespace"));
                        helm.WithChartVersion(builder.AddParameter("chartversion"));
                    });
                builder.Build().Run();
                """;
 
            var apiProgramCode = """
                var builder = WebApplication.CreateBuilder(args);
                builder.AddServiceDefaults();
                var app = builder.Build();
                app.MapDefaultEndpoints();
                app.MapGet("/test-deployment", () =>
                {
                    var first = Environment.GetEnvironmentVariable("APP_VALUE");
                    var second = Environment.GetEnvironmentVariable("SECOND_VALUE");
                    var optional = Environment.GetEnvironmentVariable("OPTIONAL_VALUE");
                    return Results.Text($"PASSED: {first}|{second}|{(optional is null ? "optional-unset" : optional)}");
                });
                app.Run();
                """;
 
            await auto.ScaffoldK8sDeployProjectAsync(
                counter,
                ProjectName,
                Path.Combine(workspace.WorkspaceRoot.FullName, ProjectName),
                appHostHostingPackages: ["Aspire.Hosting.Kubernetes"],
                apiClientPackages: [],
                appHostCode: appHostCode,
                apiProgramCode: apiProgramCode,
                output: output);
 
            await auto.AspireDeployInteractiveAsync(counter, parameterResponses:
            [
                ("registryendpoint", "localhost:5001"),
                ("namespace", k8sNamespace),
                ("chartversion", "0.1.0")
            ]);
 
            await auto.VerifyPodImageAsync(counter, k8sNamespace, $"app.kubernetes.io/component={serverName}", expectedImage);
            await auto.VerifyDeploymentAsync(counter, k8sNamespace, serverName, 18080, expectedResponse: response);
 
            // Inspect the actual pod as well as the response: the consumer must receive
            // values without mounting the producer's file or replacing its entrypoint.
            await auto.RunCommandAsync(
                $"kubectl get pods -n {k8sNamespace} -l app.kubernetes.io/component={serverName} -o json > /tmp/filekeyref-pod.json && " +
                "jq -e '.items | length == 1' /tmp/filekeyref-pod.json && " +
                "jq -e '.items[0] | " +
                "(.status.initContainerStatuses[] | select(.name == \"prepare-env\") | .state.terminated.exitCode == 0) and " +
                "(.spec.containers[0] | " +
                "([.volumeMounts[]? | select(.name == \"generated-env\")] | length == 0) and " +
                "([.command[]?] | length == 0) and " +
                "([.env[] | select(.name == \"APP_VALUE\") | .valueFrom.fileKeyRef] == " +
                "[{\"volumeName\":\"generated-env\",\"path\":\"app.env\",\"key\":\"SOURCE_KEY\",\"optional\":false}]))' /tmp/filekeyref-pod.json",
                counter);
 
            await auto.CleanupKubernetesDeploymentAsync(counter, clusterName);
        }
        finally
        {
            await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output);
            await LocalDeploymentTestHelpers.CleanupImageAsync(expectedImage, output);
            await LocalDeploymentTestHelpers.CleanupImageAsync($"{serverName}:latest", output);
        }
    }
}