55#pragma warning disable ASPIREPIPELINES003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
66
77using Aspire . Hosting . ApplicationModel ;
8+ using Aspire . Hosting . Dcp . Process ;
89using Aspire . Hosting . Docker . Resources ;
910using Aspire . Hosting . Pipelines ;
1011using Aspire . Hosting . Publishing ;
1112using Aspire . Hosting . Utils ;
1213using Microsoft . Extensions . DependencyInjection ;
14+ using Microsoft . Extensions . Logging ;
1315
1416namespace Aspire . Hosting . Docker ;
1517
@@ -31,11 +33,6 @@ public class DockerComposeEnvironmentResource : Resource, IComputeEnvironmentRes
3133 /// </summary>
3234 public string ? DefaultNetworkName { get ; set ; }
3335
34- /// <summary>
35- /// Determines whether to build container images for the resources in this environment.
36- /// </summary>
37- public bool BuildContainerImages { get ; set ; } = true ;
38-
3936 /// <summary>
4037 /// Determines whether to include an Aspire dashboard for telemetry visualization in this environment.
4138 /// </summary>
@@ -53,20 +50,120 @@ public class DockerComposeEnvironmentResource : Resource, IComputeEnvironmentRes
5350
5451 internal Dictionary < IResource , DockerComposeServiceResource > ResourceMapping { get ; } = new ( new ResourceNameComparer ( ) ) ;
5552
53+ internal EnvFile ? SharedEnvFile { get ; set ; }
54+
5655 internal PortAllocator PortAllocator { get ; } = new ( ) ;
5756
5857 /// <param name="name">The name of the Docker Compose environment.</param>
5958 public DockerComposeEnvironmentResource ( string name ) : base ( name )
6059 {
61- Annotations . Add ( new PipelineStepAnnotation ( context =>
60+ Annotations . Add ( new PipelineStepAnnotation ( async ( factoryContext ) =>
6261 {
63- var step = new PipelineStep
62+ var model = factoryContext . PipelineContext . Model ;
63+ var steps = new List < PipelineStep > ( ) ;
64+
65+ var publishStep = new PipelineStep
6466 {
6567 Name = $ "publish-{ Name } ",
6668 Action = ctx => PublishAsync ( ctx )
6769 } ;
68- step . RequiredBy ( WellKnownPipelineSteps . Publish ) ;
69- return step ;
70+ publishStep . RequiredBy ( WellKnownPipelineSteps . Publish ) ;
71+ steps . Add ( publishStep ) ;
72+
73+ // Expand deployment target steps for all compute resources
74+ foreach ( var computeResource in model . GetComputeResources ( ) )
75+ {
76+ var deploymentTarget = computeResource . GetDeploymentTargetAnnotation ( this ) ? . DeploymentTarget ;
77+
78+ if ( deploymentTarget != null && deploymentTarget . TryGetAnnotationsOfType < PipelineStepAnnotation > ( out var annotations ) )
79+ {
80+ // Resolve the deployment target's PipelineStepAnnotation and expand its steps
81+ // We do this because the deployment target is not in the model
82+ foreach ( var annotation in annotations )
83+ {
84+ var childFactoryContext = new PipelineStepFactoryContext
85+ {
86+ PipelineContext = factoryContext . PipelineContext ,
87+ Resource = deploymentTarget
88+ } ;
89+
90+ var deploymentTargetSteps = await annotation . CreateStepsAsync ( childFactoryContext ) . ConfigureAwait ( false ) ;
91+
92+ foreach ( var step in deploymentTargetSteps )
93+ {
94+ // Ensure the step is associated with the deployment target resource
95+ step . Resource ??= deploymentTarget ;
96+ }
97+
98+ steps . AddRange ( deploymentTargetSteps ) ;
99+ }
100+ }
101+ }
102+
103+ var prepareStep = new PipelineStep
104+ {
105+ Name = $ "prepare-{ Name } ",
106+ Action = ctx => PrepareAsync ( ctx )
107+ } ;
108+ prepareStep . DependsOn ( WellKnownPipelineSteps . Publish ) ;
109+ prepareStep . DependsOn ( WellKnownPipelineSteps . Build ) ;
110+ steps . Add ( prepareStep ) ;
111+
112+ var dockerComposeUpStep = new PipelineStep
113+ {
114+ Name = $ "docker-compose-up-{ Name } ",
115+ Action = ctx => DockerComposeUpAsync ( ctx ) ,
116+ Tags = [ "docker-compose-up" ] ,
117+ DependsOnSteps = [ $ "prepare-{ Name } "]
118+ } ;
119+ dockerComposeUpStep . RequiredBy ( WellKnownPipelineSteps . Deploy ) ;
120+ steps . Add ( dockerComposeUpStep ) ;
121+
122+ var dockerComposeDownStep = new PipelineStep
123+ {
124+ Name = $ "docker-compose-down-{ Name } ",
125+ Action = ctx => DockerComposeDownAsync ( ctx ) ,
126+ Tags = [ "docker-compose-down" ]
127+ } ;
128+ steps . Add ( dockerComposeDownStep ) ;
129+
130+ return steps ;
131+ } ) ) ;
132+
133+ // Add pipeline configuration annotation to wire up dependencies
134+ // This is where we wire up the build steps created by the resources
135+ Annotations . Add ( new PipelineConfigurationAnnotation ( context =>
136+ {
137+ // Wire up build step dependencies
138+ // Build steps are created by ProjectResource and ContainerResource
139+ foreach ( var computeResource in context . Model . GetComputeResources ( ) )
140+ {
141+ var deploymentTarget = computeResource . GetDeploymentTargetAnnotation ( this ) ? . DeploymentTarget ;
142+
143+ if ( deploymentTarget is null )
144+ {
145+ continue ;
146+ }
147+
148+ // Execute the PipelineConfigurationAnnotation callbacks on the deployment target
149+ if ( deploymentTarget . TryGetAnnotationsOfType < PipelineConfigurationAnnotation > ( out var annotations ) )
150+ {
151+ foreach ( var annotation in annotations )
152+ {
153+ annotation . Callback ( context ) ;
154+ }
155+ }
156+ }
157+
158+ // This ensures that resources that have to be built before deployments are handled
159+ foreach ( var computeResource in context . Model . GetBuildResources ( ) )
160+ {
161+ var buildSteps = context . GetSteps ( computeResource , WellKnownPipelineTags . BuildCompute ) ;
162+
163+ buildSteps . RequiredBy ( WellKnownPipelineSteps . Deploy )
164+ . RequiredBy ( $ "docker-compose-up-{ Name } ")
165+ . DependsOn ( WellKnownPipelineSteps . DeployPrereq ) ;
166+ }
70167 } ) ) ;
71168 }
72169
@@ -100,10 +197,174 @@ private Task PublishAsync(PipelineStepContext context)
100197 return dockerComposePublishingContext . WriteModelAsync ( context . Model , this ) ;
101198 }
102199
200+ private async Task DockerComposeUpAsync ( PipelineStepContext context )
201+ {
202+ var outputPath = PublishingContextUtils . GetEnvironmentOutputPath ( context , this ) ;
203+ var dockerComposeFilePath = Path . Combine ( outputPath , "docker-compose.yaml" ) ;
204+ var envFilePath = GetEnvFilePath ( context ) ;
205+
206+ if ( ! File . Exists ( dockerComposeFilePath ) )
207+ {
208+ throw new InvalidOperationException ( $ "Docker Compose file not found at { dockerComposeFilePath } ") ;
209+ }
210+
211+ var deployTask = await context . ReportingStep . CreateTaskAsync ( $ "Running docker compose up for **{ Name } **", context . CancellationToken ) . ConfigureAwait ( false ) ;
212+ await using ( deployTask . ConfigureAwait ( false ) )
213+ {
214+ try
215+ {
216+ var arguments = $ "compose -f \" { dockerComposeFilePath } \" ";
217+
218+ if ( File . Exists ( envFilePath ) )
219+ {
220+ arguments += $ " --env-file \" { envFilePath } \" ";
221+ }
222+
223+ arguments += " up -d --remove-orphans" ;
224+
225+ var spec = new ProcessSpec ( "docker" )
226+ {
227+ Arguments = arguments ,
228+ WorkingDirectory = outputPath ,
229+ ThrowOnNonZeroReturnCode = false ,
230+ InheritEnv = true ,
231+ OnOutputData = output =>
232+ {
233+ context . Logger . LogDebug ( "docker compose up (stdout): {Output}" , output ) ;
234+ } ,
235+ OnErrorData = error =>
236+ {
237+ context . Logger . LogDebug ( "docker compose up (stderr): {Error}" , error ) ;
238+ } ,
239+ } ;
240+
241+ var ( pendingProcessResult , processDisposable ) = ProcessUtil . Run ( spec ) ;
242+
243+ await using ( processDisposable )
244+ {
245+ var processResult = await pendingProcessResult
246+ . WaitAsync ( context . CancellationToken )
247+ . ConfigureAwait ( false ) ;
248+
249+ if ( processResult . ExitCode != 0 )
250+ {
251+ await deployTask . FailAsync ( $ "docker compose up failed with exit code { processResult . ExitCode } ", cancellationToken : context . CancellationToken ) . ConfigureAwait ( false ) ;
252+ }
253+ else
254+ {
255+ await deployTask . CompleteAsync ( $ "Docker Compose deployment complete for **{ Name } **", CompletionState . Completed , context . CancellationToken ) . ConfigureAwait ( false ) ;
256+ }
257+ }
258+ }
259+ catch ( Exception ex )
260+ {
261+ await deployTask . CompleteAsync ( $ "Docker Compose deployment failed: { ex . Message } ", CompletionState . CompletedWithError , context . CancellationToken ) . ConfigureAwait ( false ) ;
262+ throw ;
263+ }
264+ }
265+ }
266+
267+ private async Task DockerComposeDownAsync ( PipelineStepContext context )
268+ {
269+ var outputPath = PublishingContextUtils . GetEnvironmentOutputPath ( context , this ) ;
270+ var dockerComposeFilePath = Path . Combine ( outputPath , "docker-compose.yaml" ) ;
271+ var envFilePath = GetEnvFilePath ( context ) ;
272+
273+ if ( ! File . Exists ( dockerComposeFilePath ) )
274+ {
275+ throw new InvalidOperationException ( $ "Docker Compose file not found at { dockerComposeFilePath } ") ;
276+ }
277+
278+ var deployTask = await context . ReportingStep . CreateTaskAsync ( $ "Running docker compose down for **{ Name } **", context . CancellationToken ) . ConfigureAwait ( false ) ;
279+ await using ( deployTask . ConfigureAwait ( false ) )
280+ {
281+ try
282+ {
283+ var arguments = $ "compose -f \" { dockerComposeFilePath } \" ";
284+
285+ if ( File . Exists ( envFilePath ) )
286+ {
287+ arguments += $ " --env-file \" { envFilePath } \" ";
288+ }
289+
290+ arguments += " down" ;
291+
292+ var spec = new ProcessSpec ( "docker" )
293+ {
294+ Arguments = arguments ,
295+ WorkingDirectory = outputPath ,
296+ ThrowOnNonZeroReturnCode = false ,
297+ InheritEnv = true
298+ } ;
299+
300+ var ( pendingProcessResult , processDisposable ) = ProcessUtil . Run ( spec ) ;
301+
302+ await using ( processDisposable )
303+ {
304+ var processResult = await pendingProcessResult
305+ . WaitAsync ( context . CancellationToken )
306+ . ConfigureAwait ( false ) ;
307+
308+ if ( processResult . ExitCode != 0 )
309+ {
310+ await deployTask . FailAsync ( $ "docker compose down failed with exit code { processResult . ExitCode } ", cancellationToken : context . CancellationToken ) . ConfigureAwait ( false ) ;
311+ }
312+ else
313+ {
314+ await deployTask . CompleteAsync ( $ "Docker Compose shutdown complete for **{ Name } **", CompletionState . Completed , context . CancellationToken ) . ConfigureAwait ( false ) ;
315+ }
316+ }
317+ }
318+ catch ( Exception ex )
319+ {
320+ await deployTask . CompleteAsync ( $ "Docker Compose shutdown failed: { ex . Message } ", CompletionState . CompletedWithError , context . CancellationToken ) . ConfigureAwait ( false ) ;
321+ throw ;
322+ }
323+ }
324+ }
325+
326+ private async Task PrepareAsync ( PipelineStepContext context )
327+ {
328+ var envFilePath = GetEnvFilePath ( context ) ;
329+
330+ if ( CapturedEnvironmentVariables . Count == 0 || SharedEnvFile is null )
331+ {
332+ return ;
333+ }
334+
335+ foreach ( var entry in CapturedEnvironmentVariables )
336+ {
337+ var ( key , ( description , defaultValue , source ) ) = entry ;
338+
339+ if ( defaultValue is null && source is ParameterResource parameter )
340+ {
341+ defaultValue = await parameter . GetValueAsync ( context . CancellationToken ) . ConfigureAwait ( false ) ;
342+ }
343+
344+ if ( source is ContainerImageReference cir && cir . Resource . TryGetContainerImageName ( out var imageName ) )
345+ {
346+ defaultValue = imageName ;
347+ }
348+
349+ SharedEnvFile . Add ( key , defaultValue , description , onlyIfMissing : false ) ;
350+ }
351+
352+ SharedEnvFile . Save ( envFilePath , includeValues : true ) ;
353+ }
354+
103355 internal string AddEnvironmentVariable ( string name , string ? description = null , string ? defaultValue = null , object ? source = null )
104356 {
105357 CapturedEnvironmentVariables [ name ] = ( description , defaultValue , source ) ;
106358
107359 return $ "${{{name}}}";
108360 }
361+
362+ private string GetEnvFilePath ( PipelineStepContext context )
363+ {
364+ var outputPath = PublishingContextUtils . GetEnvironmentOutputPath ( context , this ) ;
365+ var hostEnvironment = context . Services . GetService < Microsoft . Extensions . Hosting . IHostEnvironment > ( ) ;
366+ var environmentName = hostEnvironment ? . EnvironmentName ?? Name ;
367+ var envFilePath = Path . Combine ( outputPath , $ "{ environmentName } .env") ;
368+ return envFilePath ;
369+ }
109370}
0 commit comments