Skip to content
Closed
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
4 changes: 2 additions & 2 deletions docs/machine-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ Whether you want to contribute to Aspire, or just use the latest build of Aspire

### WSLc (Windows only)

WSLc version 3.0.2.0 or later is supported for local development on Windows. `aspire doctor` checks the explicitly configured runtime, or reports all installed runtimes among Docker, Podman, and WSLc. Configured runtimes are marked "explicitly selected" in the report. Failures for non-selected alternatives are non-blocking when a healthy runtime is available; Docker in Windows-container mode remains an error when Docker is selected. When multiple healthy runtimes are available, Docker remains the default; set `ASPIRE_CONTAINER_RUNTIME=wslc` to select WSLc explicitly. WSLc is not supported when Aspire runs inside a Linux WSL distribution.
WSLc version 3.0.4.0 or later is supported for local development on Windows. `aspire doctor` checks the explicitly configured runtime, or reports all installed runtimes among Docker, Podman, and WSLc. Configured runtimes are marked "explicitly selected" in the report. Failures for non-selected alternatives are non-blocking when a healthy runtime is available; Docker in Windows-container mode remains an error when Docker is selected. When multiple healthy runtimes are available, Docker remains the default; set `ASPIRE_CONTAINER_RUNTIME=wslc` to select WSLc explicitly. WSLc is not supported when Aspire runs inside a Linux WSL distribution.

WSLc is currently only supported for local development. Publish and deployment support will be added soon. See the [container runtime prerequisites](https://aka.ms/aspire/containers).

Doctor reports an error when the selected WSLc client or session manager is below version 3.0.2.0, including prereleases of that minimum release, or when required version metadata cannot be validated.
Doctor reports an error when the selected WSLc client or session manager is below version 3.0.4.0, including prereleases of that minimum release, or when required version metadata cannot be validated.

`ASPIRE_CONTAINER_RUNTIME` selects a registered runtime (`docker`, `podman`, or `wslc`), not an executable path. Runtime names are case-insensitive. Leave the value blank to enable auto-detection.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal sealed class ContainerRuntimeCheck(ILogger<ContainerRuntimeCheck> logge
public const string MinimumPodmanVersion = "5.0.0";

// Matches DCP's minimum supported WSLc client and session manager version.
public const string MinimumWslcVersion = "3.0.2.0";
public const string MinimumWslcVersion = "3.0.4.0";

public int Order => 40; // Process check - more expensive

Expand Down Expand Up @@ -290,7 +290,7 @@ private static string GetContainerRuntimeInstallationLink(string runtimeKey, str
{
if (string.Equals(runtime, KnownContainerRuntimes.Wslc, StringComparisons.ContainerRuntime) && version.Revision == 0)
{
// WSLc reports clients as 3.0.2.0 and session managers as 3.0.2.
// WSLc reports clients as 3.0.4.0 and session managers as 3.0.4.
// Omit a zero revision from the minimum so either form compares correctly.
version = new Version(version.Major, version.Minor, version.Build);
}
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
Eventing.Subscribe<BeforeStartEvent>(BuiltInDistributedApplicationEventSubscriptionHandlers.MutateHttp2TransportAsync);
_innerBuilder.Services.AddKeyedSingleton<IContainerRuntime, DockerContainerRuntime>(KnownContainerRuntimes.Docker);
_innerBuilder.Services.AddKeyedSingleton<IContainerRuntime, PodmanContainerRuntime>(KnownContainerRuntimes.Podman);
_innerBuilder.Services.AddKeyedSingleton<IContainerRuntime, WslcContainerRuntime>(KnownContainerRuntimes.Wslc);
_innerBuilder.Services.AddSingleton<IContainerRuntimeResolver, ContainerRuntimeResolver>();
_innerBuilder.Services.AddSingleton<ContainerRuntimeReadiness>();
_innerBuilder.Services.AddSingleton<IResourceContainerImageManager, ResourceContainerImageManager>();
Expand Down
95 changes: 72 additions & 23 deletions src/Aspire.Hosting/Publishing/ContainerRuntimeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -449,63 +449,84 @@ protected async Task<int> ExecuteContainerCommandWithExitCodeAsync(
}

/// <summary>
/// Builds a string of build arguments for container build commands.
/// Builds arguments for container build commands without command-line quoting.
/// </summary>
/// <param name="buildArguments">The build arguments to include.</param>
/// <returns>A string containing the formatted build arguments.</returns>
protected static string BuildArgumentsString(Dictionary<string, string?> buildArguments)
/// <returns>The build argument switches and values.</returns>
protected static List<string> BuildArguments(Dictionary<string, string?> buildArguments)
{
var result = string.Empty;
var arguments = new List<string>();
foreach (var buildArg in buildArguments)
{
result += buildArg.Value is not null
? $" --build-arg \"{buildArg.Key}={buildArg.Value}\""
: $" --build-arg \"{buildArg.Key}\"";
arguments.Add("--build-arg");
arguments.Add(buildArg.Value is not null
? $"{buildArg.Key}={buildArg.Value}"
: buildArg.Key);
}
return result;

return arguments;
}

/// <summary>
/// Builds a string of build secrets for container build commands.
/// Builds secret arguments without command-line quoting.
/// </summary>
/// <param name="buildSecrets">The build secrets to include.</param>
/// <param name="useCsv">Whether the runtime parses secret specifications as CSV records.</param>
/// <param name="requireValue">Whether to require a non-null value for secrets (default: false).</param>
/// <returns>A string containing the formatted build secrets.</returns>
internal static string BuildSecretsString(Dictionary<string, BuildImageSecretValue> buildSecrets, bool requireValue = false)
/// <returns>The secret switches and specifications.</returns>
internal static List<string> BuildSecrets(Dictionary<string, BuildImageSecretValue> buildSecrets, bool useCsv, bool requireValue = false)
{
var result = string.Empty;
var arguments = new List<string>();
foreach (var buildSecret in buildSecrets)
{
arguments.Add("--secret");
var id = FormatSecretField($"id={buildSecret.Key}", useCsv);
if (buildSecret.Value.Type == BuildImageSecretType.File)
{
result += $" --secret \"id={buildSecret.Key},type=file,src={buildSecret.Value.Value}\"";
var source = FormatSecretField($"src={buildSecret.Value.Value}", useCsv);
arguments.Add($"{id},type=file,{source}");
}
else if (requireValue && buildSecret.Value.Value is null)
{
result += $" --secret \"id={buildSecret.Key},type=env\"";
arguments.Add($"{id},type=env");
}
else
{
result += $" --secret \"id={buildSecret.Key},type=env,env={buildSecret.Key.ToUpperInvariant()}\"";
var environment = FormatSecretField($"env={buildSecret.Key.ToUpperInvariant()}", useCsv);
arguments.Add($"{id},type=env,{environment}");
}
}
return result;

return arguments;
}

private static string FormatSecretField(string field, bool useCsv)
{
// Docker Buildx and WSLc parse a CSV record: id=npmrc,type=file,"src=C:\team,a\.npmrc".
// These quotes belong to the secret specification, not to process argument escaping.
// https://github.com/docker/buildx/blob/master/util/buildflags/secrets.go
if (useCsv && field.AsSpan().ContainsAny(",\"\r\n"))
{
return $"\"{field.Replace("\"", "\"\"", StringComparison.Ordinal)}\"";
}

return field;
}

/// <summary>
/// Builds a string for the target stage in container build commands.
/// Builds arguments for the target stage.
/// </summary>
/// <param name="stage">The target stage to include.</param>
/// <returns>A string containing the formatted target stage, or empty string if stage is null or empty.</returns>
protected static string BuildStageString(string? stage)
/// <returns>The target switches and value, or an empty collection.</returns>
protected static List<string> BuildStage(string? stage)
{
return !string.IsNullOrEmpty(stage) ? $" --target \"{stage}\"" : string.Empty;
return !string.IsNullOrEmpty(stage) ? ["--target", stage] : [];
}

/// <summary>
/// Executes a container runtime command and returns the process result without throwing for non-zero exit codes.
/// </summary>
protected async Task<ProcessResult> ExecuteContainerCommandWithResultAsync(
protected Task<ProcessResult> ExecuteContainerCommandWithResultAsync(
string arguments,
string errorLogTemplate,
string successLogTemplate,
Expand All @@ -514,8 +535,36 @@ protected async Task<ProcessResult> ExecuteContainerCommandWithResultAsync(
Dictionary<string, string>? environmentVariables = null,
bool retainOutput = false)
{
var spec = CreateProcessSpec(arguments, retainOutput);
return ExecuteContainerCommandWithResultAsync(
CreateProcessSpec(arguments, retainOutput), errorLogTemplate, successLogTemplate,
cancellationToken, logArguments, environmentVariables);
}

/// <summary>
/// Executes a command with argument boundaries preserved by the process runner.
/// </summary>
protected Task<ProcessResult> ExecuteContainerCommandWithResultAsync(
IReadOnlyList<string> arguments,
string errorLogTemplate,
string successLogTemplate,
CancellationToken cancellationToken,
object[] logArguments,
Dictionary<string, string>? environmentVariables = null,
bool retainOutput = false)
{
return ExecuteContainerCommandWithResultAsync(
CreateProcessSpec(arguments, retainOutput), errorLogTemplate, successLogTemplate,
cancellationToken, logArguments, environmentVariables);
}

private async Task<ProcessResult> ExecuteContainerCommandWithResultAsync(
ProcessSpec spec,
string errorLogTemplate,
string successLogTemplate,
CancellationToken cancellationToken,
object[] logArguments,
Dictionary<string, string>? environmentVariables)
{
if (environmentVariables is not null)
{
foreach (var (key, value) in environmentVariables)
Expand All @@ -524,7 +573,7 @@ protected async Task<ProcessResult> ExecuteContainerCommandWithResultAsync(
}
}

_logger.LogDebug("Running {RuntimeName} with arguments: {ArgumentList}", Name, spec.Arguments);
_logger.LogDebug("Running {RuntimeName} with arguments: {ArgumentList}", Name, spec.ArgumentList ?? (object?)spec.Arguments);
var (pendingProcessResult, processDisposable) = _processRunner.Run(spec);

await using (processDisposable)
Expand Down
13 changes: 9 additions & 4 deletions src/Aspire.Hosting/Publishing/ContainerRuntimeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,9 @@ private async Task<IContainerRuntime> ResolveInternalAsync(CancellationToken can

// Auto-detect: probe available runtimes asynchronously.
// See https://github.com/microsoft/dcp/blob/main/internal/containers/runtimes/runtime.go
// Only Docker and Podman have publishing implementations and SDK local image-store support.
var detected = await ContainerRuntimeDetector.FindAvailableRuntimeAsync(
logger: _logger,
cancellationToken: cancellationToken,
supportedRuntimes: [KnownContainerRuntimes.Docker, KnownContainerRuntimes.Podman]).ConfigureAwait(false);
cancellationToken: cancellationToken).ConfigureAwait(false);
var runtimeKey = detected?.Key ?? KnownContainerRuntimes.Docker;

if (detected is { IsHealthy: true })
Expand All @@ -96,7 +94,14 @@ private async Task<IContainerRuntime> ResolveInternalAsync(CancellationToken can
}
else
{
_logger.LogWarning("No container runtime detected, defaulting to '{DefaultRuntime}'. Install Docker or Podman to use container features.", KnownContainerRuntimes.Docker);
if (OperatingSystem.IsWindows())
{
_logger.LogWarning("No container runtime detected, defaulting to '{DefaultRuntime}'. Install Docker, Podman, or WSLc to use container features.", KnownContainerRuntimes.Docker);
}
else
{
_logger.LogWarning("No container runtime detected, defaulting to '{DefaultRuntime}'. Install Docker or Podman to use container features.", KnownContainerRuntimes.Docker);
}
}

return _serviceProvider.GetRequiredKeyedService<IContainerRuntime>(runtimeKey);
Expand Down
18 changes: 9 additions & 9 deletions src/Aspire.Hosting/Publishing/DockerContainerRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,17 @@ private async Task RunDockerBuildAsync(string contextPath, string dockerfilePath

try
{
var arguments = $"buildx build --file \"{dockerfilePath}\" --tag \"{imageName}\"";
var arguments = new List<string> { "buildx", "build", "--file", dockerfilePath, "--tag", imageName };

if (!string.IsNullOrEmpty(selectedBuilderName))
{
arguments += $" --builder \"{selectedBuilderName}\"";
arguments.AddRange(["--builder", selectedBuilderName]);
}

// Add platform support if specified
if (options?.TargetPlatform is not null)
{
arguments += $" --platform \"{options.TargetPlatform.Value.ToRuntimePlatformString()}\"";
arguments.AddRange(["--platform", options.TargetPlatform.Value.ToRuntimePlatformString()]);
}

// Add output format support if specified
Expand All @@ -95,19 +95,19 @@ private async Task RunDockerBuildAsync(string contextPath, string dockerfilePath
outputType += $",dest={archivePath}";
}

arguments += $" --output \"{outputType}\"";
arguments.AddRange(["--output", outputType]);
}

// Add build arguments if specified
arguments += BuildArgumentsString(buildArguments);
arguments.AddRange(BuildArguments(buildArguments));

// Add build secrets if specified
arguments += BuildSecretsString(buildSecrets);
arguments.AddRange(BuildSecrets(buildSecrets, useCsv: true));

// Add stage if specified
arguments += BuildStageString(stage);
arguments.AddRange(BuildStage(stage));

arguments += $" \"{contextPath}\"";
arguments.Add(contextPath);

// Prepare environment variables for build secrets
var environmentVariables = new Dictionary<string, string>();
Expand Down Expand Up @@ -179,7 +179,7 @@ public override async Task BuildImageAsync(string contextPath, string dockerfile
}

// Normalize the context path to handle trailing slashes and relative paths
var normalizedContextPath = Path.GetFullPath(contextPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var normalizedContextPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(contextPath));

await RunDockerBuildAsync(
normalizedContextPath,
Expand Down
14 changes: 7 additions & 7 deletions src/Aspire.Hosting/Publishing/PodmanContainerRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,12 @@ private async Task RunPodmanBuildAsync(string contextPath, string dockerfilePath
throw new ArgumentException("OutputPath must be provided when ImageFormat is Oci.", nameof(options));
}

var arguments = $"build --file \"{dockerfilePath}\" --tag \"{imageName}\"";
var arguments = new List<string> { "build", "--file", dockerfilePath, "--tag", imageName };

// Add platform support if specified
if (options?.TargetPlatform is not null)
{
arguments += $" --platform \"{options.TargetPlatform.Value.ToRuntimePlatformString()}\"";
arguments.AddRange(["--platform", options.TargetPlatform.Value.ToRuntimePlatformString()]);
}

// Add format support if specified
Expand All @@ -170,7 +170,7 @@ private async Task RunPodmanBuildAsync(string contextPath, string dockerfilePath
ContainerImageFormat.Docker => "docker",
_ => throw new ArgumentOutOfRangeException(nameof(options), options.ImageFormat, "Invalid container image format")
};
arguments += $" --format \"{format}\"";
arguments.AddRange(["--format", format]);
}

// Archive output is deliberately NOT handled here.
Expand All @@ -186,15 +186,15 @@ private async Task RunPodmanBuildAsync(string contextPath, string dockerfilePath
// https://docs.podman.io/en/latest/markdown/podman-save.1.html

// Add build arguments if specified
arguments += BuildArgumentsString(buildArguments);
arguments.AddRange(BuildArguments(buildArguments));

// Add build secrets if specified
arguments += BuildSecretsString(buildSecrets, requireValue: true);
arguments.AddRange(BuildSecrets(buildSecrets, useCsv: false, requireValue: true));

// Add stage if specified
arguments += BuildStageString(stage);
arguments.AddRange(BuildStage(stage));

arguments += $" \"{contextPath}\"";
arguments.Add(contextPath);

// Prepare environment variables for build secrets (only for environment-type secrets)
var environmentVariables = new Dictionary<string, string>();
Expand Down
Loading
Loading