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
2 changes: 2 additions & 0 deletions src/Aspire.Hosting/Publishing/ContainerRuntimeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ protected ContainerRuntimeBase(ILogger<TLogger> logger)

public abstract Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken);

public abstract Task<bool> SupportsMultiArchAsync(CancellationToken cancellationToken);

public abstract Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, string?> buildSecrets, string? stage, CancellationToken cancellationToken);

public virtual async Task TagImageAsync(string localImageName, string targetImageName, CancellationToken cancellationToken)
Expand Down
101 changes: 101 additions & 0 deletions src/Aspire.Hosting/Publishing/DockerContainerRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#pragma warning disable ASPIREPIPELINES003

using System.Text.Json;
using Aspire.Hosting.Dcp.Process;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -172,6 +173,106 @@ public override async Task<bool> CheckIfRunningAsync(CancellationToken cancellat
return await CheckDockerBuildxAsync(cancellationToken).ConfigureAwait(false);
}

public override async Task<bool> SupportsMultiArchAsync(CancellationToken cancellationToken)
{
try
{
var outputBuilder = new System.Text.StringBuilder();

// Run docker info --format json to get Docker daemon information
var spec = new ProcessSpec(RuntimeExecutable)
{
Arguments = "info --format json",
OnOutputData = output => outputBuilder.AppendLine(output),
OnErrorData = _ => { },
ThrowOnNonZeroReturnCode = false,
InheritEnv = true,
};

Logger.LogDebug("Checking Docker multi-arch support using 'docker info --format json'");

var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);

await using (processDisposable)
{
var processResult = await pendingProcessResult
.WaitAsync(cancellationToken)
.ConfigureAwait(false);

if (processResult.ExitCode != 0)
{
Logger.LogWarning("Failed to get Docker info. Exit code: {ExitCode}", processResult.ExitCode);
return false;
}

var dockerInfo = outputBuilder.ToString();

// Parse JSON to check for containerd image store
try
{
using var jsonDoc = JsonDocument.Parse(dockerInfo);
var root = jsonDoc.RootElement;

// Check if containerd is configured as the image store
// The containerd image store is indicated by the presence of Containerd configuration
if (root.TryGetProperty("DriverStatus", out var driverStatus) && driverStatus.ValueKind == JsonValueKind.Array)
{
foreach (var item in driverStatus.EnumerateArray())
{
if (item.ValueKind == JsonValueKind.Array && item.GetArrayLength() >= 2)
{
var key = item[0].GetString();
var value = item[1].GetString();

// Check for "Image store" entry which indicates containerd image store
if (key == "Image store" && value == "containerd")
{
Logger.LogDebug("Docker is configured with containerd image store. Multi-arch builds are supported.");
return true;
}
}
}
}

// If we have Containerd configuration with image store support, multi-arch is supported
if (root.TryGetProperty("Containerd", out _))
{
// Check if we're using the containerd image store through the Driver field
if (root.TryGetProperty("Driver", out var driver))
{
var driverValue = driver.GetString();
// When containerd image store is enabled, the driver might be "containerd" or we need additional checks
if (driverValue == "containerd")
{
Logger.LogDebug("Docker is using containerd driver. Multi-arch builds are supported.");
return true;
}
}
}

// If containerd image store is not configured, log a warning
Logger.LogWarning(
"Docker does not appear to be configured with containerd image store. " +
"Multi-arch builds require containerd image store to be enabled. " +
"You can enable it in Docker Desktop settings under 'Features in development' > 'Use containerd for pulling and storing images'. " +
"See https://docs.docker.com/engine/storage/containerd/ for more information.");

return false;
}
catch (JsonException ex)
{
Logger.LogWarning(ex, "Failed to parse Docker info JSON output");
return false;
}
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error checking Docker multi-arch support");
return false;
}
}

private async Task<bool> CheckDockerDaemonAsync(CancellationToken cancellationToken)
{
try
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Publishing/IContainerRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ internal interface IContainerRuntime
{
string Name { get; }
Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken);
Task<bool> SupportsMultiArchAsync(CancellationToken cancellationToken);
Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, string?> buildSecrets, string? stage, CancellationToken cancellationToken);
Task TagImageAsync(string localImageName, string targetImageName, CancellationToken cancellationToken);
Task RemoveImageAsync(string imageName, CancellationToken cancellationToken);
Expand Down
197 changes: 161 additions & 36 deletions src/Aspire.Hosting/Publishing/PodmanContainerRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,62 +17,154 @@ public PodmanContainerRuntime(ILogger<PodmanContainerRuntime> logger) : base(log
public override string Name => "Podman";
private async Task<int> RunPodmanBuildAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, string?> buildSecrets, string? stage, CancellationToken cancellationToken)
{
var arguments = $"build --file \"{dockerfilePath}\" --tag \"{imageName}\"";
// Check if this is a multi-platform build
var isMultiPlatform = options?.TargetPlatform is not null &&
HasMultiplePlatforms(options.TargetPlatform.Value);

// Add platform support if specified
if (options?.TargetPlatform is not null)
string? manifestName = null;

if (isMultiPlatform)
{
arguments += $" --platform \"{options.TargetPlatform.Value.ToRuntimePlatformString()}\"";
// For multi-platform builds, create a manifest
manifestName = imageName;
var createManifestResult = await CreateManifestAsync(manifestName, cancellationToken).ConfigureAwait(false);

if (createManifestResult != 0)
{
Logger.LogError("Failed to create manifest {ManifestName} with exit code {ExitCode}.", manifestName, createManifestResult);
return createManifestResult;
}
}

// Add format support if specified
if (options?.ImageFormat is not null)
try
{
var format = options.ImageFormat.Value switch
var arguments = $"build --file \"{dockerfilePath}\" --tag \"{imageName}\"";

// If we have a manifest, use it
if (manifestName is not null)
{
ContainerImageFormat.Oci => "oci",
ContainerImageFormat.Docker => "docker",
_ => throw new ArgumentOutOfRangeException(nameof(options), options.ImageFormat, "Invalid container image format")
};
arguments += $" --format \"{format}\"";
}
arguments += $" --manifest \"{manifestName}\"";
}

// Add output support if specified
if (!string.IsNullOrEmpty(options?.OutputPath))
{
// Extract resource name from imageName for the file name
var resourceName = imageName.Split('/').Last().Split(':').First();
arguments += $" --output \"{Path.Combine(options.OutputPath, resourceName)}.tar\"";
}
// Add platform support if specified
if (options?.TargetPlatform is not null)
{
arguments += $" --platform \"{options.TargetPlatform.Value.ToRuntimePlatformString()}\"";
}

// Add format support if specified
if (options?.ImageFormat is not null)
{
var format = options.ImageFormat.Value switch
{
ContainerImageFormat.Oci => "oci",
ContainerImageFormat.Docker => "docker",
_ => throw new ArgumentOutOfRangeException(nameof(options), options.ImageFormat, "Invalid container image format")
};
arguments += $" --format \"{format}\"";
}

// Add output support if specified
if (!string.IsNullOrEmpty(options?.OutputPath))
{
// Extract resource name from imageName for the file name
var resourceName = imageName.Split('/').Last().Split(':').First();
arguments += $" --output \"{Path.Combine(options.OutputPath, resourceName)}.tar\"";
}

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

// Add build arguments if specified
arguments += BuildArgumentsString(buildArguments);
// Add build secrets if specified
arguments += BuildSecretsString(buildSecrets, requireValue: true);

// Add build secrets if specified
arguments += BuildSecretsString(buildSecrets, requireValue: true);
// Add stage if specified
arguments += BuildStageString(stage);

// Add stage if specified
arguments += BuildStageString(stage);
arguments += $" \"{contextPath}\"";

arguments += $" \"{contextPath}\"";
// Prepare environment variables for build secrets
var environmentVariables = new Dictionary<string, string>();
foreach (var buildSecret in buildSecrets)
{
if (buildSecret.Value is not null)
{
environmentVariables[buildSecret.Key.ToUpperInvariant()] = buildSecret.Value;
}
}

// Prepare environment variables for build secrets
var environmentVariables = new Dictionary<string, string>();
foreach (var buildSecret in buildSecrets)
return await ExecuteContainerCommandWithExitCodeAsync(
arguments,
"Podman build for {ImageName} failed with exit code {ExitCode}.",
"Podman build for {ImageName} succeeded.",
cancellationToken,
new object[] { imageName },
environmentVariables).ConfigureAwait(false);
}
finally
{
if (buildSecret.Value is not null)
// Clean up the manifest if we created one
if (manifestName is not null && isMultiPlatform)
{
environmentVariables[buildSecret.Key.ToUpperInvariant()] = buildSecret.Value;
await RemoveManifestAsync(manifestName, cancellationToken).ConfigureAwait(false);
}
}
}

private static bool HasMultiplePlatforms(ContainerTargetPlatform platform)
{
// Count how many flags are set
var count = 0;
if (platform.HasFlag(ContainerTargetPlatform.LinuxAmd64))
{
count++;
}
if (platform.HasFlag(ContainerTargetPlatform.LinuxArm64))
{
count++;
}
if (platform.HasFlag(ContainerTargetPlatform.LinuxArm))
{
count++;
}
if (platform.HasFlag(ContainerTargetPlatform.Linux386))
{
count++;
}
if (platform.HasFlag(ContainerTargetPlatform.WindowsAmd64))
{
count++;
}
if (platform.HasFlag(ContainerTargetPlatform.WindowsArm64))
{
count++;
}

return count > 1;
}

private async Task<int> CreateManifestAsync(string manifestName, CancellationToken cancellationToken)
{
var arguments = $"manifest create \"{manifestName}\"";

return await ExecuteContainerCommandWithExitCodeAsync(
arguments,
"Podman build for {ImageName} failed with exit code {ExitCode}.",
"Podman build for {ImageName} succeeded.",
"Failed to create manifest {ManifestName} with exit code {ExitCode}.",
"Successfully created manifest {ManifestName}.",
cancellationToken,
new object[] { imageName },
environmentVariables).ConfigureAwait(false);
new object[] { manifestName }).ConfigureAwait(false);
}

private async Task<int> RemoveManifestAsync(string manifestName, CancellationToken cancellationToken)
{
var arguments = $"manifest rm \"{manifestName}\"";

return await ExecuteContainerCommandWithExitCodeAsync(
arguments,
"Failed to remove manifest {ManifestName} with exit code {ExitCode}.",
"Successfully removed manifest {ManifestName}.",
cancellationToken,
new object[] { manifestName }).ConfigureAwait(false);
}

public override async Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, string?> buildSecrets, string? stage, CancellationToken cancellationToken)
Expand Down Expand Up @@ -111,4 +203,37 @@ public override async Task<bool> CheckIfRunningAsync(CancellationToken cancellat
return false;
}
}

public override async Task<bool> SupportsMultiArchAsync(CancellationToken cancellationToken)
{
try
{
// Check if podman manifest command is available
var exitCode = await ExecuteContainerCommandWithExitCodeAsync(
"manifest --help",
"Podman manifest command failed with exit code {ExitCode}.",
"Podman manifest command is available.",
cancellationToken,
Array.Empty<object>()).ConfigureAwait(false);

if (exitCode == 0)
{
Logger.LogDebug("Podman supports manifest commands. Multi-arch builds are supported.");
return true;
}
else
{
Logger.LogWarning(
"Podman does not support manifest commands. " +
"Multi-arch builds require manifest support. " +
"Please ensure you are using a recent version of Podman.");
return false;
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error checking Podman multi-arch support");
return false;
}
}
}
Loading
Loading