File: PodmanDeploymentTests.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 Aspire.TestUtilities;
using Hex1b.Automation;
using Xunit;
 
namespace Aspire.Cli.EndToEnd.Tests;
 
/// <summary>
/// End-to-end tests for Aspire CLI deployment to Docker Compose using Podman as the container runtime.
/// Validates that setting ASPIRE_CONTAINER_RUNTIME=podman flows through to compose operations.
/// Runs Podman inside a privileged Docker helper container so the test does not depend on host Podman state.
/// </summary>
public sealed class PodmanDeploymentTests(ITestOutputHelper output)
{
    private const string ProjectName = "AspirePodmanDeployTest";
 
    [Fact]
    [OuterloopTest("Requires Docker to run a privileged Podman helper container")]
    public async Task CreateAndDeployToDockerComposeWithPodman()
    {
        var repoRoot = CliE2ETestHelpers.GetRepoRoot();
        using var workspace = TemporaryWorkspace.Create(output);
 
        var strategy = CliInstallStrategy.Detect(output.WriteLine);
        using var terminal = CliE2ETestHelpers.CreatePodmanDockerTestTerminal(repoRoot, strategy, output, 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);
 
        // Step 0: Verify Podman is available inside the helper container.
        await auto.TypeAsync("podman --version || echo 'PODMAN_NOT_FOUND'");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(10));
 
        // Step 1: Set the container runtime to Podman
        await auto.TypeAsync("export ASPIRE_CONTAINER_RUNTIME=podman");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Step 2: Create a new Aspire Starter App (no Redis cache)
        await auto.AspireNewAsync(ProjectName, counter, useRedisCache: false);
 
        // Step 3: Navigate into the project directory
        await auto.TypeAsync($"cd {ProjectName}");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Step 4: Add Aspire.Hosting.Docker package using aspire add
        await auto.TypeAsync("aspire add Aspire.Hosting.Docker");
        await auto.EnterAsync();
 
        await auto.WaitForAspireAddCompletionAsync(counter, TimeSpan.FromSeconds(180));
 
        // Step 5: Modify AppHost's main file to add Docker Compose environment
        {
            var projectDir = Path.Combine(workspace.WorkspaceRoot.FullName, ProjectName);
            var appHostDir = Path.Combine(projectDir, $"{ProjectName}.AppHost");
            var appHostFilePath = Path.Combine(appHostDir, "AppHost.cs");
 
            output.WriteLine($"Looking for AppHost.cs at: {appHostFilePath}");
 
            var content = File.ReadAllText(appHostFilePath);
 
            var buildRunPattern = "builder.Build().Run();";
            var replacement = """
// Add Docker Compose environment for deployment
builder.AddDockerComposeEnvironment("compose");
 
builder.Build().Run();
""";
 
            content = content.Replace(buildRunPattern, replacement);
            File.WriteAllText(appHostFilePath, content);
 
            output.WriteLine($"Modified AppHost.cs at: {appHostFilePath}");
        }
 
        // Step 6: Create output directory for deployment artifacts
        await auto.TypeAsync("mkdir -p deploy-output");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Step 7: Unset ASPIRE_PLAYGROUND before deploy
        await auto.TypeAsync("unset ASPIRE_PLAYGROUND");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Step 8: Run aspire deploy with Podman as the container runtime
        await auto.TypeAsync("aspire deploy -o deploy-output --non-interactive");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(5));
 
        // Step 9: Verify containers are running with podman ps
        await auto.TypeAsync("podman ps");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Step 10: Verify the application is accessible inside the helper container's network namespace.
        await auto.TypeAsync("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:$(podman ps --format '{{.Ports}}' --filter 'name=webfrontend' | grep -oE '[0-9]+->8080' | head -1 | cut -d- -f1) 2>/dev/null || echo 'request-failed'");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30));
 
        // Step 11: Clean up - destroy the deployment using aspire destroy
        await auto.AspireDestroyAsync(counter);
    }
}