Skip to content
52 changes: 32 additions & 20 deletions src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,32 +134,33 @@ public async Task BuildImagesAsync(IEnumerable<IResource> resources, ContainerBu

await using (step.ConfigureAwait(false))
{
// Currently, we build these images to the local Docker daemon. We need to ensure that
// the Docker daemon is running and accessible

var task = await step.CreateTaskAsync(
$"Checking {ContainerRuntime.Name} health",
cancellationToken).ConfigureAwait(false);

await using (task.ConfigureAwait(false))
// Only check container runtime health if there are resources that need it
if (ResourcesRequireContainerRuntime(resources, options))
{
var containerRuntimeHealthy = await ContainerRuntime.CheckIfRunningAsync(cancellationToken).ConfigureAwait(false);
var task = await step.CreateTaskAsync(
$"Checking {ContainerRuntime.Name} health",
cancellationToken).ConfigureAwait(false);

if (!containerRuntimeHealthy)
await using (task.ConfigureAwait(false))
{
logger.LogError("Container runtime is not running or is unhealthy. Cannot build container images.");
var containerRuntimeHealthy = await ContainerRuntime.CheckIfRunningAsync(cancellationToken).ConfigureAwait(false);

await task.FailAsync(
$"{ContainerRuntime.Name} is not running or is unhealthy.",
cancellationToken).ConfigureAwait(false);
if (!containerRuntimeHealthy)
{
logger.LogError("Container runtime is not running or is unhealthy. Cannot build container images.");

await step.CompleteAsync("Building container images failed", CompletionState.CompletedWithError, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException("Container runtime is not running or is unhealthy.");
}
await task.FailAsync(
$"{ContainerRuntime.Name} is not running or is unhealthy.",
cancellationToken).ConfigureAwait(false);

await task.SucceedAsync(
$"{ContainerRuntime.Name} is healthy.",
cancellationToken).ConfigureAwait(false);
await step.CompleteAsync("Building container images failed", CompletionState.CompletedWithError, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException("Container runtime is not running or is unhealthy.");
}

await task.SucceedAsync(
$"{ContainerRuntime.Name} is healthy.",
cancellationToken).ConfigureAwait(false);
}
}

foreach (var resource in resources)
Expand Down Expand Up @@ -386,6 +387,17 @@ await ContainerRuntime.BuildImageAsync(
return await step.CreateTaskAsync(description, cancellationToken).ConfigureAwait(false);
}

// .NET Container builds that push OCI images to a local file path do not need a runtime
internal static bool ResourcesRequireContainerRuntime(IEnumerable<IResource> resources, ContainerBuildOptions? options)
{
var hasDockerfileResources = resources.Any(resource =>
resource.TryGetLastAnnotation<ContainerImageAnnotation>(out _) &&
resource.TryGetLastAnnotation<DockerfileBuildAnnotation>(out _));
var usesDocker = options == null || options.ImageFormat == ContainerImageFormat.Docker;
var hasNoOutputPath = options?.OutputPath == null;
return hasDockerfileResources || usesDocker || hasNoOutputPath;
}

}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,66 @@ public void ContainerBuildOptions_CanSetAllProperties()
Assert.Equal(ContainerTargetPlatform.LinuxArm64, options.TargetPlatform);
}

[Fact]
public async Task BuildImagesAsync_WithOnlyProjectResourcesAndOci_DoesNotNeedContainerRuntime()
{
using var builder = TestDistributedApplicationBuilder.Create(output);

builder.Services.AddLogging(logging =>
{
logging.AddFakeLogging();
logging.AddXunit(output);
});

// Create a fake container runtime that would fail if called
var fakeContainerRuntime = new FakeContainerRuntime(shouldFail: true);
builder.Services.AddKeyedSingleton<IContainerRuntime>("docker", fakeContainerRuntime);

var servicea = builder.AddProject<Projects.ServiceA>("servicea");

using var app = builder.Build();

using var cts = new CancellationTokenSource(TestConstants.LongTimeoutTimeSpan);
var imageBuilder = app.Services.GetRequiredService<IResourceContainerImageBuilder>();

// This should not fail despite the fake container runtime being configured to fail
// because we only have project resources (no DockerfileBuildAnnotation)
var options = new ContainerBuildOptions { ImageFormat = ContainerImageFormat.Oci, OutputPath = "/tmp/test-path" };
await imageBuilder.BuildImagesAsync([servicea.Resource], options: options, cts.Token);

// Validate that the container runtime health check was not called
Assert.False(fakeContainerRuntime.WasHealthCheckCalled);
}

[Fact]
public async Task BuildImagesAsync_WithDockerfileResources_ChecksContainerRuntimeHealth()
{
using var builder = TestDistributedApplicationBuilder.Create(output);

builder.Services.AddLogging(logging =>
{
logging.AddFakeLogging();
logging.AddXunit(output);
});

// Create a fake container runtime that tracks health check calls
var fakeContainerRuntime = new FakeContainerRuntime(shouldFail: false);
builder.Services.AddKeyedSingleton<IContainerRuntime>("docker", fakeContainerRuntime);

var (tempContextPath, tempDockerfilePath) = await DockerfileUtils.CreateTemporaryDockerfileAsync();
var dockerfileResource = builder.AddDockerfile("test-dockerfile", tempContextPath, tempDockerfilePath);

using var app = builder.Build();

using var cts = new CancellationTokenSource(TestConstants.LongTimeoutTimeSpan);
var imageBuilder = app.Services.GetRequiredService<IResourceContainerImageBuilder>();

await imageBuilder.BuildImagesAsync([dockerfileResource.Resource], options: null, cts.Token);

// Validate that the container runtime health check was called for resources with DockerfileBuildAnnotation
Assert.True(fakeContainerRuntime.WasHealthCheckCalled);
}

[Fact]
public async Task BuildImageAsync_ThrowsInvalidOperationException_WhenDockerRuntimeNotAvailable()
{
Expand All @@ -394,7 +454,7 @@ public async Task BuildImageAsync_ThrowsInvalidOperationException_WhenDockerRunt
logging.AddXunit(output);
});

builder.Services.AddKeyedSingleton<IContainerRuntime>("docker", new UnhealthyMockContainerRuntime());
builder.Services.AddKeyedSingleton<IContainerRuntime>("docker", new FakeContainerRuntime(shouldFail: true));

var (tempContextPath, tempDockerfilePath) = await DockerfileUtils.CreateTemporaryDockerfileAsync();
var container = builder.AddDockerfile("container", tempContextPath, tempDockerfilePath);
Expand All @@ -415,18 +475,21 @@ public async Task BuildImageAsync_ThrowsInvalidOperationException_WhenDockerRunt
Assert.Contains(logs, log => log.Message.Contains("Container runtime is not running or is unhealthy. Cannot build container images."));
}

private sealed class UnhealthyMockContainerRuntime : IContainerRuntime
private sealed class FakeContainerRuntime(bool shouldFail) : IContainerRuntime
{
public string Name => "MockDocker";
public string Name => "fake-runtime";
public bool WasHealthCheckCalled { get; private set; }

public Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken)
{
return Task.FromResult(false);
WasHealthCheckCalled = true;
return Task.FromResult(!shouldFail);
}

public Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, CancellationToken cancellationToken)
{
throw new InvalidOperationException("This mock runtime should not be used for building images.");
// For testing, we don't need to actually build anything
return Task.CompletedTask;
}
}
}
Loading