Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -382,6 +382,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 @@ -393,7 +453,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 @@ -414,18 +474,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