File: ResourceFailureLoggingTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Containers.Tests\Aspire.Hosting.Containers.Tests.csproj (Aspire.Hosting.Containers.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.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Aspire.TestUtilities;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using TestConstants = Microsoft.AspNetCore.InternalTesting.TestConstants;
 
namespace Aspire.Hosting.Containers.Tests;
 
[Trait("Partition", "6")]
public class ContainerResourceFailureLoggingTests(ITestOutputHelper testOutputHelper)
{
    [Fact]
    [RequiresFeature(TestFeature.ContainerRuntime)]
    public async Task BadContainerRuntimeArg()
    {
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.DefaultOrchestratorTestLongTimeout);
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        var container = builder.AddContainer("container", "nginx")
            .WithContainerRuntimeArgs("--illegal");
        AddFakeLogging(container);
 
        FakeLogCollector logCollector;
        using (var app = builder.Build())
        {
            logCollector = app.Services.GetFakeLogCollector();
            await app.StartAsync(cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
            await app.ResourceNotifications.WaitForResourceAsync(container.Resource.Name, KnownResourceStates.FailedToStart, cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
        }
 
        var logLines = GetLogLines(logCollector);
        Assert.Contains(logLines, x => x.EndsWith("unknown flag: --illegal"));
    }
 
    [Fact]
    [RequiresFeature(TestFeature.ContainerRuntime)]
    public async Task BadImage()
    {
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.DefaultOrchestratorTestLongTimeout);
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        var container = builder.AddContainer("container", "does-not-exist")
            .WithImageRegistry("does.not.exist.internal")
            .WithImagePullPolicy(ImagePullPolicy.Always);
        AddFakeLogging(container);
 
        FakeLogCollector logCollector;
        using (var app = builder.Build())
        {
            logCollector = app.Services.GetFakeLogCollector();
            await app.StartAsync(cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
            await app.ResourceNotifications.WaitForResourceAsync(container.Resource.Name, KnownResourceStates.FailedToStart, cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
        }
 
        var logLines = GetLogLines(logCollector);
        Assert.Contains(logLines, ContainerRuntimeLogPatterns.IsImagePullFailure);
    }
 
    [Fact]
    [RequiresFeature(TestFeature.ContainerRuntime)]
    public async Task NeedsAuthentication()
    {
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.DefaultOrchestratorTestLongTimeout);
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        var container = builder.AddContainer("container", "mattermost.com/go-msft-fips:1.24.6")
            .WithImageRegistry("cgr.dev")
            .WithImagePullPolicy(ImagePullPolicy.Always);
        AddFakeLogging(container);
 
        FakeLogCollector logCollector;
        using (var app = builder.Build())
        {
            logCollector = app.Services.GetFakeLogCollector();
            await app.StartAsync(cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
            await app.ResourceNotifications.WaitForResourceAsync(container.Resource.Name, KnownResourceStates.FailedToStart, cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
        }
 
        var logLines = GetLogLines(logCollector);
        Assert.Contains(logLines, ContainerRuntimeLogPatterns.IsImagePullFailure);
    }
 
    [Fact]
    [RequiresFeature(TestFeature.ContainerRuntime)]
    public async Task ContainerExitsImmediatelyAfterStart()
    {
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.DefaultOrchestratorTestLongTimeout);
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        var container = builder.AddContainer("container", "nginx")
            .WithArgs("--illegal-argument");
        AddFakeLogging(container);
 
        FakeLogCollector logCollector;
        using (var app = builder.Build())
        {
            logCollector = app.Services.GetFakeLogCollector();
            await app.StartAsync(cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
            await app.ResourceNotifications.WaitForResourceAsync(container.Resource.Name, [KnownResourceStates.Exited, KnownResourceStates.FailedToStart], cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
 
            var logLines = GetLogLines(logCollector);
            AssertSingleLogLine(
                logLines,
                x => x.Contains("exec: --illegal-argument: not found"),
                "container startup failure from nginx entrypoint");
        }
    }
 
    private static void AddFakeLogging<T>(IResourceBuilder<T> builder)
        where T : IResource
    {
        var category = $"{builder.ApplicationBuilder.Environment.ApplicationName}.Resources.{builder.Resource.Name}";
        builder.ApplicationBuilder.Services.AddLogging(x => x.AddFakeLogging(y => y.FilteredCategories.Add(category)));
    }
 
    private static List<string> GetLogLines(FakeLogCollector logCollector)
    {
        return [.. logCollector.GetSnapshot()
                .Select(x => x.StructuredState?.SingleOrDefault(x => x.Key == "LineContent"))
                .Where(x => x is not null)
                .Select(x => x?.Value!)];
    }
 
    private static void AssertSingleLogLine(List<string> logLines, Func<string, bool> predicate, string expected)
    {
        var matches = logLines.Where(predicate).ToList();
        Assert.True(
            matches.Count == 1,
            $"Expected exactly one log line for {expected}, but found {matches.Count}.{Environment.NewLine}Captured resource logs:{Environment.NewLine}{FormatLogLines(logLines)}");
    }
 
    private static string FormatLogLines(List<string> logLines)
    {
        return logLines.Count == 0
            ? "<none>"
            : string.Join(Environment.NewLine, logLines.Select((line, index) => $"{index + 1}: {line}"));
    }
}