Skip to content

Commit f2bedf4

Browse files
authored
Add ContainerBuildOptions support to ResourceContainerImageBuilder for customizing dotnet publish (#10074)
1 parent 9fe8413 commit f2bedf4

File tree

9 files changed

+790
-135
lines changed

9 files changed

+790
-135
lines changed

src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ private async Task WriteDockerComposeOutputAsync(DistributedApplicationModel mod
123123
// Build container images for the services that require it
124124
if (containerImagesToBuild.Count > 0)
125125
{
126-
await ImageBuilder.BuildImagesAsync(containerImagesToBuild, cancellationToken).ConfigureAwait(false);
126+
await ImageBuilder.BuildImagesAsync(containerImagesToBuild, options: null, cancellationToken).ConfigureAwait(false);
127127
}
128128

129129
var step = await activityReporter.CreateStepAsync(

src/Aspire.Hosting/CompatibilitySuppressions.xml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,23 @@
4343
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
4444
<IsBaselineSuppression>true</IsBaselineSuppression>
4545
</Suppression>
46+
<Suppression>
47+
<DiagnosticId>CP0002</DiagnosticId>
48+
<Target>M:Aspire.Hosting.Publishing.IResourceContainerImageBuilder.BuildImageAsync(Aspire.Hosting.ApplicationModel.IResource,System.Threading.CancellationToken)</Target>
49+
<Left>lib/net8.0/Aspire.Hosting.dll</Left>
50+
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
51+
<IsBaselineSuppression>true</IsBaselineSuppression>
52+
</Suppression>
53+
<Suppression>
54+
<DiagnosticId>CP0006</DiagnosticId>
55+
<Target>M:Aspire.Hosting.Publishing.IResourceContainerImageBuilder.BuildImageAsync(Aspire.Hosting.ApplicationModel.IResource,Aspire.Hosting.Publishing.ContainerBuildOptions,System.Threading.CancellationToken)</Target>
56+
<Left>lib/net8.0/Aspire.Hosting.dll</Left>
57+
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
58+
<IsBaselineSuppression>true</IsBaselineSuppression>
59+
</Suppression>
4660
<Suppression>
4761
<DiagnosticId>CP0006</DiagnosticId>
48-
<Target>M:Aspire.Hosting.Publishing.IResourceContainerImageBuilder.BuildImagesAsync(System.Collections.Generic.IEnumerable{Aspire.Hosting.ApplicationModel.IResource},System.Threading.CancellationToken)</Target>
62+
<Target>M:Aspire.Hosting.Publishing.IResourceContainerImageBuilder.BuildImagesAsync(System.Collections.Generic.IEnumerable{Aspire.Hosting.ApplicationModel.IResource},Aspire.Hosting.Publishing.ContainerBuildOptions,System.Threading.CancellationToken)</Target>
4963
<Left>lib/net8.0/Aspire.Hosting.dll</Left>
5064
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
5165
<IsBaselineSuppression>true</IsBaselineSuppression>
Lines changed: 186 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIREPUBLISHERS001
5+
46
using Aspire.Hosting.Dcp.Process;
57
using Microsoft.Extensions.Logging;
68

@@ -9,49 +11,118 @@ namespace Aspire.Hosting.Publishing;
911
internal sealed class DockerContainerRuntime(ILogger<DockerContainerRuntime> logger) : IContainerRuntime
1012
{
1113
public string Name => "Docker";
12-
private async Task<int> RunDockerBuildAsync(string contextPath, string dockerfilePath, string imageName, CancellationToken cancellationToken)
14+
private async Task<int> RunDockerBuildAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, CancellationToken cancellationToken)
1315
{
14-
var spec = new ProcessSpec("docker")
16+
string? builderName = null;
17+
var resourceName = imageName.Replace('/', '-').Replace(':', '-');
18+
19+
// Docker requires a custom buildkit instance for the image when
20+
// targeting the OCI format so we construct it and remove it here.
21+
if (options?.ImageFormat == ContainerImageFormat.Oci)
1522
{
16-
Arguments = $"build --file {dockerfilePath} --tag {imageName} {contextPath}",
17-
OnOutputData = output =>
23+
if (string.IsNullOrEmpty(options?.OutputPath))
1824
{
19-
logger.LogInformation("docker build (stdout): {Output}", output);
20-
},
21-
OnErrorData = error =>
22-
{
23-
logger.LogInformation("docker build (stderr): {Error}", error);
24-
},
25-
ThrowOnNonZeroReturnCode = false,
26-
InheritEnv = true
27-
};
25+
throw new ArgumentException("OutputPath must be provided when ImageFormat is Oci.", nameof(options));
26+
}
2827

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

32-
await using (processDisposable)
31+
if (createBuilderResult != 0)
32+
{
33+
logger.LogError("Failed to create buildkit instance {BuilderName} with exit code {ExitCode}.", builderName, createBuilderResult);
34+
return createBuilderResult;
35+
}
36+
}
37+
38+
try
3339
{
34-
var processResult = await pendingProcessResult
35-
.WaitAsync(cancellationToken)
36-
.ConfigureAwait(false);
40+
var arguments = $"buildx build --file \"{dockerfilePath}\" --tag \"{imageName}\"";
3741

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

44-
logger.LogInformation("Docker build for {ImageName} succeeded.", imageName);
45-
return processResult.ExitCode;
48+
// Add platform support if specified
49+
if (options?.TargetPlatform is not null)
50+
{
51+
arguments += $" --platform \"{options.TargetPlatform.Value.ToRuntimePlatformString()}\"";
52+
}
53+
54+
// Add output format support if specified
55+
if (options?.ImageFormat is not null || !string.IsNullOrEmpty(options?.OutputPath))
56+
{
57+
var outputType = options?.ImageFormat switch
58+
{
59+
ContainerImageFormat.Oci => "type=oci",
60+
ContainerImageFormat.Docker => "type=docker",
61+
null => "type=docker",
62+
_ => throw new ArgumentOutOfRangeException(nameof(options), options.ImageFormat, "Invalid container image format")
63+
};
64+
65+
if (!string.IsNullOrEmpty(options?.OutputPath))
66+
{
67+
outputType += $",dest={Path.Combine(options.OutputPath, resourceName)}.tar";
68+
}
69+
70+
arguments += $" --output \"{outputType}\"";
71+
}
72+
73+
arguments += $" \"{contextPath}\"";
74+
75+
var spec = new ProcessSpec("docker")
76+
{
77+
Arguments = arguments,
78+
OnOutputData = output =>
79+
{
80+
logger.LogInformation("docker buildx (stdout): {Output}", output);
81+
},
82+
OnErrorData = error =>
83+
{
84+
logger.LogInformation("docker buildx (stderr): {Error}", error);
85+
},
86+
ThrowOnNonZeroReturnCode = false,
87+
InheritEnv = true
88+
};
89+
90+
logger.LogInformation("Running Docker CLI with arguments: {ArgumentList}", spec.Arguments);
91+
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);
92+
93+
await using (processDisposable)
94+
{
95+
var processResult = await pendingProcessResult
96+
.WaitAsync(cancellationToken)
97+
.ConfigureAwait(false);
98+
99+
if (processResult.ExitCode != 0)
100+
{
101+
logger.LogError("docker buildx for {ImageName} failed with exit code {ExitCode}.", imageName, processResult.ExitCode);
102+
return processResult.ExitCode;
103+
}
104+
105+
logger.LogInformation("docker buildx for {ImageName} succeeded.", imageName);
106+
return processResult.ExitCode;
107+
}
108+
}
109+
finally
110+
{
111+
// Clean up the buildkit instance if we created one
112+
if (!string.IsNullOrEmpty(builderName))
113+
{
114+
await RemoveBuildkitInstanceAsync(builderName, cancellationToken).ConfigureAwait(false);
115+
}
46116
}
47117
}
48118

49-
public async Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, CancellationToken cancellationToken)
119+
public async Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, CancellationToken cancellationToken)
50120
{
51121
var exitCode = await RunDockerBuildAsync(
52122
contextPath,
53123
dockerfilePath,
54124
imageName,
125+
options,
55126
cancellationToken).ConfigureAwait(false);
56127

57128
if (exitCode != 0)
@@ -64,14 +135,14 @@ public Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken)
64135
{
65136
var spec = new ProcessSpec("docker")
66137
{
67-
Arguments = "info",
138+
Arguments = "buildx version",
68139
OnOutputData = output =>
69140
{
70-
logger.LogInformation("docker info (stdout): {Output}", output);
141+
logger.LogInformation("docker buildx version (stdout): {Output}", output);
71142
},
72143
OnErrorData = error =>
73144
{
74-
logger.LogInformation("docker info (stderr): {Error}", error);
145+
logger.LogInformation("docker buildx version (stderr): {Error}", error);
75146
},
76147
ThrowOnNonZeroReturnCode = false,
77148
InheritEnv = true
@@ -80,24 +151,105 @@ public Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken)
80151
logger.LogInformation("Running Docker CLI with arguments: {ArgumentList}", spec.Arguments);
81152
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);
82153

83-
return CheckDockerInfoAsync(pendingProcessResult, processDisposable, cancellationToken);
154+
return CheckDockerBuildxAsync(pendingProcessResult, processDisposable, cancellationToken);
84155

85-
async Task<bool> CheckDockerInfoAsync(Task<ProcessResult> pendingResult, IAsyncDisposable processDisposable, CancellationToken ct)
156+
async Task<bool> CheckDockerBuildxAsync(Task<ProcessResult> pendingResult, IAsyncDisposable processDisposable, CancellationToken ct)
86157
{
87158
await using (processDisposable)
88159
{
89160
var processResult = await pendingResult.WaitAsync(ct).ConfigureAwait(false);
90161

91162
if (processResult.ExitCode != 0)
92163
{
93-
logger.LogError("Docker info failed with exit code {ExitCode}.", processResult.ExitCode);
164+
logger.LogError("Docker buildx version failed with exit code {ExitCode}.", processResult.ExitCode);
94165
return false;
95166
}
96167

97-
// Optionally, parse output for health, but exit code 0 is usually sufficient.
98-
logger.LogInformation("Docker is running and healthy.");
168+
logger.LogInformation("Docker buildx is available and running.");
99169
return true;
100170
}
101171
}
102172
}
103-
}
173+
174+
private async Task<int> CreateBuildkitInstanceAsync(string builderName, CancellationToken cancellationToken)
175+
{
176+
var arguments = $"buildx create --name \"{builderName}\" --driver docker-container";
177+
178+
var spec = new ProcessSpec("docker")
179+
{
180+
Arguments = arguments,
181+
OnOutputData = output =>
182+
{
183+
logger.LogInformation("docker buildx create (stdout): {Output}", output);
184+
},
185+
OnErrorData = error =>
186+
{
187+
logger.LogInformation("docker buildx create (stderr): {Error}", error);
188+
},
189+
ThrowOnNonZeroReturnCode = false,
190+
InheritEnv = true
191+
};
192+
193+
logger.LogInformation("Creating buildkit instance with arguments: {ArgumentList}", spec.Arguments);
194+
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);
195+
196+
await using (processDisposable)
197+
{
198+
var processResult = await pendingProcessResult
199+
.WaitAsync(cancellationToken)
200+
.ConfigureAwait(false);
201+
202+
if (processResult.ExitCode != 0)
203+
{
204+
logger.LogError("Failed to create buildkit instance {BuilderName} with exit code {ExitCode}.", builderName, processResult.ExitCode);
205+
}
206+
else
207+
{
208+
logger.LogInformation("Successfully created buildkit instance {BuilderName}.", builderName);
209+
}
210+
211+
return processResult.ExitCode;
212+
}
213+
}
214+
215+
private async Task<int> RemoveBuildkitInstanceAsync(string builderName, CancellationToken cancellationToken)
216+
{
217+
var arguments = $"buildx rm \"{builderName}\"";
218+
219+
var spec = new ProcessSpec("docker")
220+
{
221+
Arguments = arguments,
222+
OnOutputData = output =>
223+
{
224+
logger.LogInformation("docker buildx rm (stdout): {Output}", output);
225+
},
226+
OnErrorData = error =>
227+
{
228+
logger.LogInformation("docker buildx rm (stderr): {Error}", error);
229+
},
230+
ThrowOnNonZeroReturnCode = false,
231+
InheritEnv = true
232+
};
233+
234+
logger.LogInformation("Removing buildkit instance with arguments: {ArgumentList}", spec.Arguments);
235+
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);
236+
237+
await using (processDisposable)
238+
{
239+
var processResult = await pendingProcessResult
240+
.WaitAsync(cancellationToken)
241+
.ConfigureAwait(false);
242+
243+
if (processResult.ExitCode != 0)
244+
{
245+
logger.LogWarning("Failed to remove buildkit instance {BuilderName} with exit code {ExitCode}.", builderName, processResult.ExitCode);
246+
}
247+
else
248+
{
249+
logger.LogInformation("Successfully removed buildkit instance {BuilderName}.", builderName);
250+
}
251+
252+
return processResult.ExitCode;
253+
}
254+
}
255+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIREPUBLISHERS001
5+
46
namespace Aspire.Hosting.Publishing;
57

68
internal interface IContainerRuntime
79
{
810
string Name { get; }
911
Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken);
10-
public Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, CancellationToken cancellationToken);
12+
public Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, CancellationToken cancellationToken);
1113
}

0 commit comments

Comments
 (0)