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+
46using Aspire . Hosting . Dcp . Process ;
57using Microsoft . Extensions . Logging ;
68
@@ -9,49 +11,118 @@ namespace Aspire.Hosting.Publishing;
911internal 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+ }
0 commit comments