Skip to content

Commit 5886086

Browse files
CopilotdavidfowlCopilotcaptainsafia
authored
Register IPipelineOutputService in DI for pipeline output directory management (#12563)
* Initial plan * Set default OutputPath and add IntermediateOutputPath at PipelineContext level Co-authored-by: davidfowl <[email protected]> * Refactor output path handling to use IPipelineOutputService - Create IPipelineOutputService interface with GetOutputDirectory() and GetTempDirectory() methods - Implement PipelineOutputService with better naming than IntermediateOutputPath - Expose service via PipelineContext.OutputService and PipelineStepContext.OutputService - Keep backward compatible OutputPath property for existing consumers - Remove direct IntermediateOutputPath property per feedback Co-authored-by: davidfowl <[email protected]> * Add resource-specific directory support to IPipelineOutputService - Add GetOutputDirectory(IResource) overload for resource-specific output directories - Add GetTempDirectory(IResource) overload for resource-specific temp directories - Implementation creates subdirectories using resource.Name under base directories - Enables per-resource isolation of output and temp artifacts Co-authored-by: davidfowl <[email protected]> * Remove OutputPath property from PipelineContext and PipelineStepContext - Remove OutputPath convenience property per @davidfowl feedback - Consumers now explicitly call OutputService.GetOutputDirectory() - Update AzureEnvironmentResource to use OutputService.GetOutputDirectory() - Update PublishingContextUtils to use OutputService.GetOutputDirectory() - Update test helper to use OutputService.GetOutputDirectory() - Keep OutputService property for explicit access to output management Co-authored-by: davidfowl <[email protected]> * Remove OutputService from PipelineContext and resolve from DI - Register IPipelineOutputService in DI container with PipelineOptions - Remove OutputService property from PipelineContext and PipelineStepContext - Remove outputPath parameter from PipelineContext constructor - Update all consumers to resolve IPipelineOutputService from Services - AzureEnvironmentResource - PublishingContextUtils - JsonDocumentManifestPublisher test helper - Remove IOptions<PipelineOptions> dependency from PipelineExecutor - Steps now explicitly resolve IPipelineOutputService when needed Co-authored-by: davidfowl <[email protected]> * Update PipelineOutputService to inject dependencies and use resource-specific overload - PipelineOutputService now injects IOptions<PipelineOptions> and IConfiguration directly in constructor - Simplified DI registration to use standard AddSingleton without factory lambda - PublishingContextUtils uses GetOutputDirectory(environment) for resource-specific paths - Removes manual path combination logic in favor of service method Co-authored-by: davidfowl <[email protected]> * Update src/Shared/PublishingContextUtils.cs Co-authored-by: Copilot <[email protected]> * Add XML documentation and update diagnostic ID to ASPIREPIPELINES004 - Update Experimental attribute from ASPIREPIPELINES001 to ASPIREPIPELINES004 - Add XML documentation for GetOutputDirectory() default behavior - Add XML documentation for private fields in PipelineOutputService - Document purpose of AppHost:PathSha256 usage in CreateTempDirectory - Suppress ASPIREPIPELINES004 diagnostic in all consuming files Co-authored-by: captainsafia <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: davidfowl <[email protected]> Co-authored-by: David Fowler <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: captainsafia <[email protected]>
1 parent 11b8fe3 commit 5886086

File tree

13 files changed

+145
-31
lines changed

13 files changed

+145
-31
lines changed

src/Aspire.Cli/Commands/PublishCommand.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,10 @@ protected override string[] GetRunArguments(string? fullyQualifiedOutputPath, st
4949
{
5050
var baseArgs = new List<string> { "--operation", "publish", "--step", "publish" };
5151

52-
var targetPath = fullyQualifiedOutputPath is not null
53-
? fullyQualifiedOutputPath
54-
: Path.Combine(Environment.CurrentDirectory, "aspire-output");
55-
56-
baseArgs.AddRange(["--output-path", targetPath]);
52+
if (fullyQualifiedOutputPath is not null)
53+
{
54+
baseArgs.AddRange(["--output-path", fullyQualifiedOutputPath]);
55+
}
5756

5857
// Add --log-level and --envionment flags if specified
5958
var logLevel = parseResult.GetValue(_logLevelOption);

src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#pragma warning disable ASPIREAZURE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
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
#pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
7+
#pragma warning disable ASPIREPIPELINES004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
78

89
using System.Diagnostics.CodeAnalysis;
910
using Aspire.Hosting.ApplicationModel;
@@ -121,8 +122,9 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet
121122
private Task PublishAsync(PipelineStepContext context)
122123
{
123124
var azureProvisioningOptions = context.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>();
125+
var outputService = context.Services.GetRequiredService<IPipelineOutputService>();
124126
var publishingContext = new AzurePublishingContext(
125-
context.OutputPath ?? throw new InvalidOperationException("OutputPath is required for Azure publishing."),
127+
outputService.GetOutputDirectory(),
126128
azureProvisioningOptions.Value,
127129
context.Services,
128130
context.Logger,

src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ internal sealed class DockerComposePublishingContext(
3737
UnixFileMode.OtherRead | UnixFileMode.OtherWrite;
3838

3939
public readonly IResourceContainerImageBuilder ImageBuilder = imageBuilder;
40-
public readonly string OutputPath = outputPath ?? throw new InvalidOperationException("OutputPath is required for Docker Compose publishing.");
40+
public readonly string OutputPath = outputPath;
4141

4242
internal async Task WriteModelAsync(DistributedApplicationModel model, DockerComposeEnvironmentResource environment)
4343
{

src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal sealed class KubernetesPublishingContext(
2020
ILogger logger,
2121
CancellationToken cancellationToken = default)
2222
{
23-
public readonly string OutputPath = outputPath ?? throw new InvalidOperationException("OutputPath is required for Kubernetes publishing.");
23+
public readonly string OutputPath = outputPath;
2424

2525
private readonly Dictionary<string, Dictionary<string, object>> _helmValues = new()
2626
{

src/Aspire.Hosting/DistributedApplicationBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#pragma warning disable ASPIREPIPELINES003
55
#pragma warning disable ASPIREPIPELINES001
66
#pragma warning disable ASPIREPIPELINES002
7+
#pragma warning disable ASPIREPIPELINES004
78

89
using System.Diagnostics;
910
using System.Reflection;
@@ -462,6 +463,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
462463
_innerBuilder.Services.AddSingleton<IResourceContainerImageBuilder, ResourceContainerImageBuilder>();
463464
_innerBuilder.Services.AddSingleton<PipelineActivityReporter>();
464465
_innerBuilder.Services.AddSingleton<IPipelineActivityReporter, PipelineActivityReporter>(sp => sp.GetRequiredService<PipelineActivityReporter>());
466+
_innerBuilder.Services.AddSingleton<IPipelineOutputService, PipelineOutputService>();
465467
_innerBuilder.Services.AddSingleton(Pipeline);
466468

467469
// Configure pipeline logging options
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using Aspire.Hosting.ApplicationModel;
6+
7+
namespace Aspire.Hosting.Pipelines;
8+
9+
/// <summary>
10+
/// Service for managing pipeline output directories.
11+
/// </summary>
12+
[Experimental("ASPIREPIPELINES004", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
13+
public interface IPipelineOutputService
14+
{
15+
/// <summary>
16+
/// Gets the output directory for deployment artifacts.
17+
/// If no output path is configured, defaults to <c>{CurrentDirectory}/aspire-output</c>.
18+
/// </summary>
19+
/// <returns>The path to the output directory for deployment artifacts.</returns>
20+
string GetOutputDirectory();
21+
22+
/// <summary>
23+
/// Gets the output directory for a specific resource's deployment artifacts.
24+
/// </summary>
25+
/// <param name="resource">The resource to get the output directory for.</param>
26+
/// <returns>The path to the output directory for the resource's deployment artifacts.</returns>
27+
string GetOutputDirectory(IResource resource);
28+
29+
/// <summary>
30+
/// Gets a temporary directory for build artifacts.
31+
/// </summary>
32+
/// <returns>The path to a temporary directory for build artifacts.</returns>
33+
string GetTempDirectory();
34+
35+
/// <summary>
36+
/// Gets a temporary directory for a specific resource's build artifacts.
37+
/// </summary>
38+
/// <param name="resource">The resource to get the temporary directory for.</param>
39+
/// <returns>The path to a temporary directory for the resource's build artifacts.</returns>
40+
string GetTempDirectory(IResource resource);
41+
}

src/Aspire.Hosting/Pipelines/PipelineContext.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@ namespace Aspire.Hosting.Pipelines;
1515
/// <param name="serviceProvider">The service provider for dependency resolution.</param>
1616
/// <param name="logger">The logger for pipeline operations.</param>
1717
/// <param name="cancellationToken">The cancellation token for the pipeline operation.</param>
18-
/// <param name="outputPath">The output path for deployment artifacts.</param>
1918
[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
2019
public sealed class PipelineContext(
2120
DistributedApplicationModel model,
2221
DistributedApplicationExecutionContext executionContext,
2322
IServiceProvider serviceProvider,
2423
ILogger logger,
25-
CancellationToken cancellationToken,
26-
string? outputPath)
24+
CancellationToken cancellationToken)
2725
{
2826
/// <summary>
2927
/// Gets the distributed application model to be deployed.
@@ -49,9 +47,4 @@ public sealed class PipelineContext(
4947
/// Gets the cancellation token for the pipeline operation.
5048
/// </summary>
5149
public CancellationToken CancellationToken { get; set; } = cancellationToken;
52-
53-
/// <summary>
54-
/// Gets the output path for deployment artifacts.
55-
/// </summary>
56-
public string? OutputPath { get; } = outputPath;
5750
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using Aspire.Hosting.ApplicationModel;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.Options;
8+
9+
namespace Aspire.Hosting.Pipelines;
10+
11+
/// <summary>
12+
/// Default implementation of <see cref="IPipelineOutputService"/>.
13+
/// </summary>
14+
[Experimental("ASPIREPIPELINES004", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
15+
internal sealed class PipelineOutputService : IPipelineOutputService
16+
{
17+
/// <summary>
18+
/// Stores the resolved output directory path, or <c>null</c> if not specified.
19+
/// </summary>
20+
private readonly string? _outputPath;
21+
22+
/// <summary>
23+
/// Lazily creates and stores the path to the temporary directory for pipeline output.
24+
/// </summary>
25+
private readonly Lazy<string> _tempDirectory;
26+
27+
public PipelineOutputService(IOptions<PipelineOptions> options, IConfiguration configuration)
28+
{
29+
_outputPath = options.Value.OutputPath is not null ? Path.GetFullPath(options.Value.OutputPath) : null;
30+
_tempDirectory = new Lazy<string>(() => CreateTempDirectory(configuration));
31+
}
32+
33+
/// <inheritdoc/>
34+
public string GetOutputDirectory()
35+
{
36+
return _outputPath ?? Path.Combine(Environment.CurrentDirectory, "aspire-output");
37+
}
38+
39+
/// <inheritdoc/>
40+
public string GetOutputDirectory(IResource resource)
41+
{
42+
ArgumentNullException.ThrowIfNull(resource);
43+
44+
var baseOutputDir = GetOutputDirectory();
45+
return Path.Combine(baseOutputDir, resource.Name);
46+
}
47+
48+
/// <inheritdoc/>
49+
public string GetTempDirectory()
50+
{
51+
return _tempDirectory.Value;
52+
}
53+
54+
/// <inheritdoc/>
55+
public string GetTempDirectory(IResource resource)
56+
{
57+
ArgumentNullException.ThrowIfNull(resource);
58+
59+
var baseTempDir = GetTempDirectory();
60+
return Path.Combine(baseTempDir, resource.Name);
61+
}
62+
63+
/// <summary>
64+
/// Creates a temporary directory for pipeline build artifacts.
65+
/// Uses AppHost:PathSha256 from configuration to create an isolated temp directory per app host,
66+
/// enabling multiple app hosts to run concurrently without conflicts.
67+
/// If AppHost:PathSha256 is not available, falls back to a generic "aspire" temp directory.
68+
/// </summary>
69+
private static string CreateTempDirectory(IConfiguration configuration)
70+
{
71+
var appHostSha = configuration["AppHost:PathSha256"];
72+
73+
if (!string.IsNullOrEmpty(appHostSha))
74+
{
75+
return Directory.CreateTempSubdirectory($"aspire-{appHostSha}").FullName;
76+
}
77+
78+
// Fallback if AppHost:PathSha256 is not available
79+
return Directory.CreateTempSubdirectory("aspire").FullName;
80+
}
81+
}

src/Aspire.Hosting/Pipelines/PipelineStepContext.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,4 @@ public sealed class PipelineStepContext
5454
/// Gets the cancellation token for the pipeline operation.
5555
/// </summary>
5656
public CancellationToken CancellationToken => PipelineContext.CancellationToken;
57-
58-
/// <summary>
59-
/// Gets the output path for deployment artifacts.
60-
/// </summary>
61-
public string? OutputPath => PipelineContext.OutputPath;
6257
}

src/Aspire.Hosting/Publishing/PipelineExecutor.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using Microsoft.Extensions.DependencyInjection;
1313
using Microsoft.Extensions.Hosting;
1414
using Microsoft.Extensions.Logging;
15-
using Microsoft.Extensions.Options;
1615

1716
namespace Aspire.Hosting.Publishing;
1817

@@ -25,7 +24,6 @@ internal sealed class PipelineExecutor(
2524
IPipelineActivityReporter activityReporter,
2625
IDistributedApplicationEventing eventing,
2726
BackchannelService backchannelService,
28-
IOptions<PipelineOptions> options,
2927
IPipelineActivityReporter pipelineActivityReporter) : BackgroundService
3028
{
3129
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -99,8 +97,7 @@ await eventing.PublishAsync<AfterPublishEvent>(
9997

10098
public async Task ExecutePipelineAsync(DistributedApplicationModel model, CancellationToken cancellationToken)
10199
{
102-
var pipelineContext = new PipelineContext(model, executionContext, serviceProvider, logger, cancellationToken, options.Value.OutputPath is not null ?
103-
Path.GetFullPath(options.Value.OutputPath) : null);
100+
var pipelineContext = new PipelineContext(model, executionContext, serviceProvider, logger, cancellationToken);
104101

105102
var pipeline = serviceProvider.GetRequiredService<IDistributedApplicationPipeline>();
106103
await pipeline.ExecuteAsync(pipelineContext).ConfigureAwait(false);

0 commit comments

Comments
 (0)