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
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ private async Task WriteDockerComposeOutputAsync(DistributedApplicationModel mod
// Build container images for the services that require it
if (containerImagesToBuild.Count > 0)
{
await ImageBuilder.BuildImagesAsync(containerImagesToBuild, cancellationToken).ConfigureAwait(false);
await ImageBuilder.BuildImagesAsync(containerImagesToBuild, options: null, cancellationToken).ConfigureAwait(false);
}

var step = await activityReporter.CreateStepAsync(
Expand Down
16 changes: 15 additions & 1 deletion src/Aspire.Hosting/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,23 @@
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Aspire.Hosting.Publishing.IResourceContainerImageBuilder.BuildImageAsync(Aspire.Hosting.ApplicationModel.IResource,System.Threading.CancellationToken)</Target>
<Left>lib/net8.0/Aspire.Hosting.dll</Left>
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Aspire.Hosting.Publishing.IResourceContainerImageBuilder.BuildImageAsync(Aspire.Hosting.ApplicationModel.IResource,Aspire.Hosting.Publishing.ContainerBuildOptions,System.Threading.CancellationToken)</Target>
<Left>lib/net8.0/Aspire.Hosting.dll</Left>
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Aspire.Hosting.Publishing.IResourceContainerImageBuilder.BuildImagesAsync(System.Collections.Generic.IEnumerable{Aspire.Hosting.ApplicationModel.IResource},System.Threading.CancellationToken)</Target>
<Target>M:Aspire.Hosting.Publishing.IResourceContainerImageBuilder.BuildImagesAsync(System.Collections.Generic.IEnumerable{Aspire.Hosting.ApplicationModel.IResource},Aspire.Hosting.Publishing.ContainerBuildOptions,System.Threading.CancellationToken)</Target>
<Left>lib/net8.0/Aspire.Hosting.dll</Left>
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
Expand Down
220 changes: 186 additions & 34 deletions src/Aspire.Hosting/Publishing/DockerContainerRuntime.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIREPUBLISHERS001

using Aspire.Hosting.Dcp.Process;
using Microsoft.Extensions.Logging;

Expand All @@ -9,49 +11,118 @@ namespace Aspire.Hosting.Publishing;
internal sealed class DockerContainerRuntime(ILogger<DockerContainerRuntime> logger) : IContainerRuntime
{
public string Name => "Docker";
private async Task<int> RunDockerBuildAsync(string contextPath, string dockerfilePath, string imageName, CancellationToken cancellationToken)
private async Task<int> RunDockerBuildAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, CancellationToken cancellationToken)
{
var spec = new ProcessSpec("docker")
string? builderName = null;
var resourceName = imageName.Replace('/', '-').Replace(':', '-');

// Docker requires a custom buildkit instance for the image when
// targeting the OCI format so we construct it and remove it here.
if (options?.ImageFormat == ContainerImageFormat.Oci)
{
Arguments = $"build --file {dockerfilePath} --tag {imageName} {contextPath}",
OnOutputData = output =>
if (string.IsNullOrEmpty(options?.OutputPath))
{
logger.LogInformation("docker build (stdout): {Output}", output);
},
OnErrorData = error =>
{
logger.LogInformation("docker build (stderr): {Error}", error);
},
ThrowOnNonZeroReturnCode = false,
InheritEnv = true
};
throw new ArgumentException("OutputPath must be provided when ImageFormat is Oci.", nameof(options));
}

logger.LogInformation("Running Docker CLI with arguments: {ArgumentList}", spec.Arguments);
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);
builderName = $"{resourceName}-builder";
var createBuilderResult = await CreateBuildkitInstanceAsync(builderName, cancellationToken).ConfigureAwait(false);

await using (processDisposable)
if (createBuilderResult != 0)
{
logger.LogError("Failed to create buildkit instance {BuilderName} with exit code {ExitCode}.", builderName, createBuilderResult);
return createBuilderResult;
}
}

try
{
var processResult = await pendingProcessResult
.WaitAsync(cancellationToken)
.ConfigureAwait(false);
var arguments = $"buildx build --file \"{dockerfilePath}\" --tag \"{imageName}\"";

if (processResult.ExitCode != 0)
// Use the specific builder for OCI builds
if (!string.IsNullOrEmpty(builderName))
{
logger.LogError("Docker build for {ImageName} failed with exit code {ExitCode}.", imageName, processResult.ExitCode);
return processResult.ExitCode;
arguments += $" --builder \"{builderName}\"";
}

logger.LogInformation("Docker build for {ImageName} succeeded.", imageName);
return processResult.ExitCode;
// Add platform support if specified
if (options?.TargetPlatform is not null)
{
arguments += $" --platform \"{options.TargetPlatform.Value.ToRuntimePlatformString()}\"";
}

// Add output format support if specified
if (options?.ImageFormat is not null || !string.IsNullOrEmpty(options?.OutputPath))
{
var outputType = options?.ImageFormat switch
{
ContainerImageFormat.Oci => "type=oci",
ContainerImageFormat.Docker => "type=docker",
null => "type=docker",
_ => throw new ArgumentOutOfRangeException(nameof(options), options.ImageFormat, "Invalid container image format")
};

if (!string.IsNullOrEmpty(options?.OutputPath))
{
outputType += $",dest={Path.Combine(options.OutputPath, resourceName)}.tar";
}

arguments += $" --output \"{outputType}\"";
}

arguments += $" \"{contextPath}\"";

var spec = new ProcessSpec("docker")
{
Arguments = arguments,
OnOutputData = output =>
{
logger.LogInformation("docker buildx (stdout): {Output}", output);
},
OnErrorData = error =>
{
logger.LogInformation("docker buildx (stderr): {Error}", error);
},
ThrowOnNonZeroReturnCode = false,
InheritEnv = true
};

logger.LogInformation("Running Docker CLI with arguments: {ArgumentList}", spec.Arguments);
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);

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

if (processResult.ExitCode != 0)
{
logger.LogError("docker buildx for {ImageName} failed with exit code {ExitCode}.", imageName, processResult.ExitCode);
return processResult.ExitCode;
}

logger.LogInformation("docker buildx for {ImageName} succeeded.", imageName);
return processResult.ExitCode;
}
}
finally
{
// Clean up the buildkit instance if we created one
if (!string.IsNullOrEmpty(builderName))
{
await RemoveBuildkitInstanceAsync(builderName, cancellationToken).ConfigureAwait(false);
}
}
}

public async Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, CancellationToken cancellationToken)
public async Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, CancellationToken cancellationToken)
{
var exitCode = await RunDockerBuildAsync(
contextPath,
dockerfilePath,
imageName,
options,
cancellationToken).ConfigureAwait(false);

if (exitCode != 0)
Expand All @@ -64,14 +135,14 @@ public Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken)
{
var spec = new ProcessSpec("docker")
{
Arguments = "info",
Arguments = "buildx version",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this change from info to buildx version?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're taking a dependency on buildx now to support all the output types and buildx is an add-on that's included by default in certain implementations. docker info isn't a sufficient enough check.

OnOutputData = output =>
{
logger.LogInformation("docker info (stdout): {Output}", output);
logger.LogInformation("docker buildx version (stdout): {Output}", output);
},
OnErrorData = error =>
{
logger.LogInformation("docker info (stderr): {Error}", error);
logger.LogInformation("docker buildx version (stderr): {Error}", error);
},
ThrowOnNonZeroReturnCode = false,
InheritEnv = true
Expand All @@ -80,24 +151,105 @@ public Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken)
logger.LogInformation("Running Docker CLI with arguments: {ArgumentList}", spec.Arguments);
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);

return CheckDockerInfoAsync(pendingProcessResult, processDisposable, cancellationToken);
return CheckDockerBuildxAsync(pendingProcessResult, processDisposable, cancellationToken);

async Task<bool> CheckDockerInfoAsync(Task<ProcessResult> pendingResult, IAsyncDisposable processDisposable, CancellationToken ct)
async Task<bool> CheckDockerBuildxAsync(Task<ProcessResult> pendingResult, IAsyncDisposable processDisposable, CancellationToken ct)
{
await using (processDisposable)
{
var processResult = await pendingResult.WaitAsync(ct).ConfigureAwait(false);

if (processResult.ExitCode != 0)
{
logger.LogError("Docker info failed with exit code {ExitCode}.", processResult.ExitCode);
logger.LogError("Docker buildx version failed with exit code {ExitCode}.", processResult.ExitCode);
return false;
}

// Optionally, parse output for health, but exit code 0 is usually sufficient.
logger.LogInformation("Docker is running and healthy.");
logger.LogInformation("Docker buildx is available and running.");
return true;
}
}
}
}

private async Task<int> CreateBuildkitInstanceAsync(string builderName, CancellationToken cancellationToken)
{
var arguments = $"buildx create --name \"{builderName}\" --driver docker-container";

var spec = new ProcessSpec("docker")
{
Arguments = arguments,
OnOutputData = output =>
{
logger.LogInformation("docker buildx create (stdout): {Output}", output);
},
OnErrorData = error =>
{
logger.LogInformation("docker buildx create (stderr): {Error}", error);
},
ThrowOnNonZeroReturnCode = false,
InheritEnv = true
};

logger.LogInformation("Creating buildkit instance with arguments: {ArgumentList}", spec.Arguments);
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);

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

if (processResult.ExitCode != 0)
{
logger.LogError("Failed to create buildkit instance {BuilderName} with exit code {ExitCode}.", builderName, processResult.ExitCode);
}
else
{
logger.LogInformation("Successfully created buildkit instance {BuilderName}.", builderName);
}

return processResult.ExitCode;
}
}

private async Task<int> RemoveBuildkitInstanceAsync(string builderName, CancellationToken cancellationToken)
{
var arguments = $"buildx rm \"{builderName}\"";

var spec = new ProcessSpec("docker")
{
Arguments = arguments,
OnOutputData = output =>
{
logger.LogInformation("docker buildx rm (stdout): {Output}", output);
},
OnErrorData = error =>
{
logger.LogInformation("docker buildx rm (stderr): {Error}", error);
},
ThrowOnNonZeroReturnCode = false,
InheritEnv = true
};

logger.LogInformation("Removing buildkit instance with arguments: {ArgumentList}", spec.Arguments);
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 remove buildkit instance {BuilderName} with exit code {ExitCode}.", builderName, processResult.ExitCode);
}
else
{
logger.LogInformation("Successfully removed buildkit instance {BuilderName}.", builderName);
}

return processResult.ExitCode;
}
}
}
4 changes: 3 additions & 1 deletion src/Aspire.Hosting/Publishing/IContainerRuntime.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIREPUBLISHERS001

namespace Aspire.Hosting.Publishing;

internal interface IContainerRuntime
{
string Name { get; }
Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken);
public Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, CancellationToken cancellationToken);
public Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, CancellationToken cancellationToken);
}
Loading
Loading