Skip to content

Commit 00abc75

Browse files
davidfowlCopilot
andcommitted
Replace sync-over-async runtime resolution with IContainerRuntimeResolver
Introduce IContainerRuntimeResolver with async ResolveAsync() that caches the result. This eliminates the .GetAwaiter().GetResult() call in the DI singleton factory that blocked the thread during startup while probing container runtimes. Callers now resolve IContainerRuntimeResolver and await ResolveAsync() instead of resolving IContainerRuntime directly. This is a breaking change for the experimental IContainerRuntime API surface. Also consolidates version detection into ContainerRuntimeDetector (AOT-friendly JsonDocument parsing), adds FindBestRuntime() for reuse without re-probing, and slims ContainerRuntimeCheck to pure policy checks with no process spawning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c32116d commit 00abc75

File tree

17 files changed

+414
-317
lines changed

17 files changed

+414
-317
lines changed

src/Aspire.Cli/JsonSourceGenerationContext.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ namespace Aspire.Cli;
2727
[JsonSerializable(typeof(DoctorCheckResponse))]
2828
[JsonSerializable(typeof(EnvironmentCheckResult))]
2929
[JsonSerializable(typeof(DoctorCheckSummary))]
30-
[JsonSerializable(typeof(ContainerVersionJson))]
3130
[JsonSerializable(typeof(AspireJsonConfiguration))]
3231
[JsonSerializable(typeof(AspireConfigFile))]
3332
[JsonSerializable(typeof(List<DevCertInfo>))]

src/Aspire.Cli/Utils/EnvironmentChecker/ContainerRuntimeCheck.cs

Lines changed: 80 additions & 222 deletions
Large diffs are not rendered by default.

src/Aspire.Hosting.Azure/AcrLoginService.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ internal sealed class AcrLoginService : IAcrLoginService
2626
};
2727

2828
private readonly IHttpClientFactory _httpClientFactory;
29-
private readonly IContainerRuntime _containerRuntime;
29+
private readonly IContainerRuntimeResolver _containerRuntimeResolver;
3030
private readonly ILogger<AcrLoginService> _logger;
3131

3232
private sealed class AcrRefreshTokenResponse
@@ -42,12 +42,12 @@ private sealed class AcrRefreshTokenResponse
4242
/// Initializes a new instance of the <see cref="AcrLoginService"/> class.
4343
/// </summary>
4444
/// <param name="httpClientFactory">The HTTP client factory for making OAuth2 exchange requests.</param>
45-
/// <param name="containerRuntime">The container runtime for performing registry login.</param>
45+
/// <param name="containerRuntimeResolver">The container runtime resolver for performing registry login.</param>
4646
/// <param name="logger">The logger for diagnostic output.</param>
47-
public AcrLoginService(IHttpClientFactory httpClientFactory, IContainerRuntime containerRuntime, ILogger<AcrLoginService> logger)
47+
public AcrLoginService(IHttpClientFactory httpClientFactory, IContainerRuntimeResolver containerRuntimeResolver, ILogger<AcrLoginService> logger)
4848
{
4949
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
50-
_containerRuntime = containerRuntime ?? throw new ArgumentNullException(nameof(containerRuntime));
50+
_containerRuntimeResolver = containerRuntimeResolver ?? throw new ArgumentNullException(nameof(containerRuntimeResolver));
5151
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
5252
}
5353

@@ -76,7 +76,8 @@ public async Task LoginAsync(
7676
_logger.LogDebug("ACR refresh token acquired, length: {TokenLength}", refreshToken.Length);
7777

7878
// Step 3: Login to the registry using container runtime
79-
await _containerRuntime.LoginToRegistryAsync(registryEndpoint, AcrUsername, refreshToken, cancellationToken).ConfigureAwait(false);
79+
var containerRuntime = await _containerRuntimeResolver.ResolveAsync(cancellationToken).ConfigureAwait(false);
80+
await containerRuntime.LoginToRegistryAsync(registryEndpoint, AcrUsername, refreshToken, cancellationToken).ConfigureAwait(false);
8081
}
8182

8283
private async Task<string> ExchangeAadTokenForAcrRefreshTokenAsync(

src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ private async Task DockerComposeUpAsync(PipelineStepContext context)
223223
throw new InvalidOperationException($"Docker Compose file not found at {dockerComposeFilePath}");
224224
}
225225

226-
var runtime = context.Services.GetRequiredService<IContainerRuntime>();
226+
var runtime = await context.Services.GetRequiredService<IContainerRuntimeResolver>().ResolveAsync(context.CancellationToken).ConfigureAwait(false);
227227

228228
var deployTask = await context.ReportingStep.CreateTaskAsync(
229229
new MarkdownString($"Running compose up for **{Name}** using **{runtime.Name}**"),
@@ -259,7 +259,7 @@ private async Task DockerComposeDownAsync(PipelineStepContext context)
259259
throw new InvalidOperationException($"Docker Compose file not found at {dockerComposeFilePath}");
260260
}
261261

262-
var runtime = context.Services.GetRequiredService<IContainerRuntime>();
262+
var runtime = await context.Services.GetRequiredService<IContainerRuntimeResolver>().ResolveAsync(context.CancellationToken).ConfigureAwait(false);
263263

264264
var deployTask = await context.ReportingStep.CreateTaskAsync(
265265
new MarkdownString($"Running compose down for **{Name}** using **{runtime.Name}**"),

src/Aspire.Hosting.Docker/DockerComposeServiceResource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ private async Task PrintEndpointsAsync(PipelineStepContext context, DockerCompos
335335
}
336336

337337
// Query the running containers for published ports
338-
var runtime = context.Services.GetRequiredService<IContainerRuntime>();
338+
var runtime = await context.Services.GetRequiredService<IContainerRuntimeResolver>().ResolveAsync(context.CancellationToken).ConfigureAwait(false);
339339
var composeContext = environment.CreateComposeOperationContext(context);
340340
var services = await runtime.ComposeListServicesAsync(composeContext, context.CancellationToken).ConfigureAwait(false);
341341

src/Aspire.Hosting/ApplicationModel/ProjectResource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ private async Task BuildProjectImage(PipelineStepContext ctx)
144144
var tempTag = $"temp-{Guid.NewGuid():N}";
145145
var tempImageName = $"{originalImageName}:{tempTag}";
146146

147-
var containerRuntime = ctx.Services.GetRequiredService<IContainerRuntime>();
147+
var containerRuntime = await ctx.Services.GetRequiredService<IContainerRuntimeResolver>().ResolveAsync(ctx.CancellationToken).ConfigureAwait(false);
148148

149149
logger.LogDebug("Tagging image {OriginalImageName} as {TempImageName}", originalImageName, tempImageName);
150150
await containerRuntime.TagImageAsync(originalImageName, tempImageName, ctx.CancellationToken).ConfigureAwait(false);

src/Aspire.Hosting/DistributedApplicationBuilder.cs

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -511,38 +511,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
511511
Eventing.Subscribe<BeforeStartEvent>(BuiltInDistributedApplicationEventSubscriptionHandlers.MutateHttp2TransportAsync);
512512
_innerBuilder.Services.AddKeyedSingleton<IContainerRuntime, DockerContainerRuntime>("docker");
513513
_innerBuilder.Services.AddKeyedSingleton<IContainerRuntime, PodmanContainerRuntime>("podman");
514-
_innerBuilder.Services.AddSingleton(sp =>
515-
{
516-
var dcpOptions = sp.GetRequiredService<IOptions<DcpOptions>>();
517-
var logger = sp.GetRequiredService<ILoggerFactory>().CreateLogger("Aspire.Hosting.ContainerRuntime");
518-
var configuredRuntime = dcpOptions.Value.ContainerRuntime;
519-
520-
if (configuredRuntime is not null)
521-
{
522-
logger.LogInformation("Container runtime '{RuntimeKey}' configured via ASPIRE_CONTAINER_RUNTIME.", configuredRuntime);
523-
return sp.GetRequiredKeyedService<IContainerRuntime>(configuredRuntime);
524-
}
525-
526-
// Auto-detect: probe available runtimes, matching DCP's detection logic.
527-
// See https://github.com/microsoft/dcp/blob/main/internal/containers/runtimes/runtime.go
528-
var detected = ContainerRuntimeDetector.FindAvailableRuntimeAsync().GetAwaiter().GetResult();
529-
var runtimeKey = detected?.Executable ?? "docker";
530-
531-
if (detected is { IsHealthy: true })
532-
{
533-
logger.LogInformation("Container runtime auto-detected: {RuntimeName} ({Executable}).", detected.Name, detected.Executable);
534-
}
535-
else if (detected is { IsInstalled: true })
536-
{
537-
logger.LogWarning("Container runtime '{RuntimeName}' is installed but not running. {Error}", detected.Name, detected.Error);
538-
}
539-
else
540-
{
541-
logger.LogWarning("No container runtime detected, defaulting to 'docker'. Install Docker or Podman to use container features.");
542-
}
543-
544-
return sp.GetRequiredKeyedService<IContainerRuntime>(runtimeKey);
545-
});
514+
_innerBuilder.Services.AddSingleton<IContainerRuntimeResolver, ContainerRuntimeResolver>();
546515
_innerBuilder.Services.AddSingleton<IResourceContainerImageManager, ResourceContainerImageManager>();
547516
_innerBuilder.Services.AddSingleton<PipelineActivityReporter>();
548517
_innerBuilder.Services.AddSingleton<IPipelineActivityReporter, PipelineActivityReporter>(sp => sp.GetRequiredService<PipelineActivityReporter>());

src/Aspire.Hosting/Pipelines/PipelineStepHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private static async Task TagImageForLocalRegistryAsync(IResource resource, Pipe
8484
: resource.Name.ToLowerInvariant();
8585

8686
// Only tag the image, don't push to a remote registry
87-
var containerRuntime = context.Services.GetRequiredService<IContainerRuntime>();
87+
var containerRuntime = await context.Services.GetRequiredService<IContainerRuntimeResolver>().ResolveAsync(context.CancellationToken).ConfigureAwait(false);
8888
await containerRuntime.TagImageAsync(localImageName, targetTag, context.CancellationToken).ConfigureAwait(false);
8989

9090
await tagTask.CompleteAsync(
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#pragma warning disable ASPIRECONTAINERRUNTIME001
5+
6+
using Aspire.Hosting.Dcp;
7+
using Aspire.Shared;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.Extensions.Options;
11+
12+
namespace Aspire.Hosting.Publishing;
13+
14+
/// <summary>
15+
/// Resolves the container runtime asynchronously using explicit configuration or auto-detection.
16+
/// Caches the result after first resolution.
17+
/// </summary>
18+
internal sealed class ContainerRuntimeResolver : IContainerRuntimeResolver
19+
{
20+
private readonly IServiceProvider _serviceProvider;
21+
private readonly IOptions<DcpOptions> _dcpOptions;
22+
private readonly ILogger _logger;
23+
private Task<IContainerRuntime>? _cachedTask;
24+
25+
public ContainerRuntimeResolver(
26+
IServiceProvider serviceProvider,
27+
IOptions<DcpOptions> dcpOptions,
28+
ILoggerFactory loggerFactory)
29+
{
30+
_serviceProvider = serviceProvider;
31+
_dcpOptions = dcpOptions;
32+
_logger = loggerFactory.CreateLogger("Aspire.Hosting.ContainerRuntime");
33+
}
34+
35+
public Task<IContainerRuntime> ResolveAsync(CancellationToken cancellationToken = default)
36+
{
37+
return _cachedTask ??= ResolveInternalAsync(cancellationToken);
38+
}
39+
40+
private async Task<IContainerRuntime> ResolveInternalAsync(CancellationToken cancellationToken)
41+
{
42+
var configuredRuntime = _dcpOptions.Value.ContainerRuntime;
43+
44+
if (configuredRuntime is not null)
45+
{
46+
_logger.LogInformation("Container runtime '{RuntimeKey}' configured via ASPIRE_CONTAINER_RUNTIME.", configuredRuntime);
47+
return _serviceProvider.GetRequiredKeyedService<IContainerRuntime>(configuredRuntime);
48+
}
49+
50+
// Auto-detect: probe available runtimes asynchronously.
51+
// See https://github.com/microsoft/dcp/blob/main/internal/containers/runtimes/runtime.go
52+
var detected = await ContainerRuntimeDetector.FindAvailableRuntimeAsync(logger: _logger, cancellationToken: cancellationToken).ConfigureAwait(false);
53+
var runtimeKey = detected?.Executable ?? "docker";
54+
55+
if (detected is { IsHealthy: true })
56+
{
57+
_logger.LogInformation("Container runtime auto-detected: {RuntimeName} ({Executable}).", detected.Name, detected.Executable);
58+
}
59+
else if (detected is { IsInstalled: true })
60+
{
61+
_logger.LogWarning("Container runtime '{RuntimeName}' is installed but not running. {Error}", detected.Name, detected.Error);
62+
}
63+
else
64+
{
65+
_logger.LogWarning("No container runtime detected, defaulting to 'docker'. Install Docker or Podman to use container features.");
66+
}
67+
68+
return _serviceProvider.GetRequiredKeyedService<IContainerRuntime>(runtimeKey);
69+
}
70+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace Aspire.Hosting.Publishing;
7+
8+
/// <summary>
9+
/// Resolves the configured or auto-detected container runtime asynchronously.
10+
/// The result is cached after the first resolution.
11+
/// </summary>
12+
[Experimental("ASPIRECONTAINERRUNTIME001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
13+
public interface IContainerRuntimeResolver
14+
{
15+
/// <summary>
16+
/// Resolves the container runtime, detecting it from the environment if not explicitly configured.
17+
/// The result is cached after the first call.
18+
/// </summary>
19+
/// <param name="cancellationToken">A token to cancel the operation.</param>
20+
/// <returns>The resolved container runtime.</returns>
21+
Task<IContainerRuntime> ResolveAsync(CancellationToken cancellationToken = default);
22+
}

0 commit comments

Comments
 (0)