Skip to content

Commit b2f90eb

Browse files
Copilotcaptainsafiaeerhardt
authored
Add support for pipeline step tagging and configuration phase dependency management (#12293)
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com> Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Safia Abdalla <safia@safia.rocks>
1 parent b4fde1f commit b2f90eb

File tree

11 files changed

+635
-18
lines changed

11 files changed

+635
-18
lines changed

playground/pipelines/Pipelines.AppHost/AppHost.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ await assignRoleTask.CompleteAsync(
225225
context.CancellationToken).ConfigureAwait(false);
226226
}
227227
}
228-
}, requiredBy: "upload-bind-mounts", dependsOn: WellKnownPipelineSteps.ProvisionInfrastructure);
228+
}, requiredBy: "upload-bind-mounts", dependsOn: WellKnownPipelineTags.ProvisionInfrastructure);
229229

230230
builder.Pipeline.AddStep("upload-bind-mounts", async (deployingContext) =>
231231
{
@@ -324,7 +324,7 @@ await uploadTask.CompleteAsync(
324324
totalUploads += fileCount;
325325
}
326326
}
327-
}, requiredBy: WellKnownPipelineSteps.DeployCompute, dependsOn: WellKnownPipelineSteps.ProvisionInfrastructure);
327+
}, requiredBy: WellKnownPipelineTags.DeployCompute, dependsOn: WellKnownPipelineTags.ProvisionInfrastructure);
328328

329329
builder.AddProject<Projects.Publishers_ApiService>("api-service")
330330
.WithComputeEnvironment(aasEnv)

playground/pipelines/Pipelines.Library/DistributedApplicationPipelineExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ await DeployProjectToAppServiceAsync(
5050
}
5151
}
5252
}
53-
}, dependsOn: WellKnownPipelineSteps.DeployCompute);
53+
}, dependsOn: WellKnownPipelineTags.DeployCompute);
5454

5555
return pipeline;
5656
}

src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,17 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet
8282

8383
var provisionStep = new PipelineStep
8484
{
85-
Name = WellKnownPipelineSteps.ProvisionInfrastructure,
86-
Action = ctx => ProvisionAzureBicepResourcesAsync(ctx, provisioningContext!)
85+
Name = "provision-azure-bicep-resources",
86+
Action = ctx => ProvisionAzureBicepResourcesAsync(ctx, provisioningContext!),
87+
Tags = [WellKnownPipelineTags.ProvisionInfrastructure]
8788
};
8889
provisionStep.DependsOn(createContextStep);
8990

9091
var buildStep = new PipelineStep
9192
{
92-
Name = WellKnownPipelineSteps.BuildCompute,
93-
Action = ctx => BuildContainerImagesAsync(ctx)
93+
Name = "build-container-images",
94+
Action = ctx => BuildContainerImagesAsync(ctx),
95+
Tags = [WellKnownPipelineTags.BuildCompute]
9496
};
9597

9698
var pushStep = new PipelineStep
@@ -103,8 +105,9 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet
103105

104106
var deployStep = new PipelineStep
105107
{
106-
Name = WellKnownPipelineSteps.DeployCompute,
107-
Action = ctx => DeployComputeResourcesAsync(ctx, provisioningContext!)
108+
Name = "deploy-compute-resources",
109+
Action = ctx => DeployComputeResourcesAsync(ctx, provisioningContext!),
110+
Tags = [WellKnownPipelineTags.DeployCompute]
108111
};
109112
deployStep.DependsOn(pushStep);
110113
deployStep.DependsOn(provisionStep);

src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Globalization;
99
using System.Runtime.ExceptionServices;
1010
using System.Text;
11+
using Aspire.Hosting.ApplicationModel;
1112
using Microsoft.Extensions.DependencyInjection;
1213
using Microsoft.Extensions.Logging.Abstractions;
1314

@@ -17,6 +18,7 @@ namespace Aspire.Hosting.Pipelines;
1718
internal sealed class DistributedApplicationPipeline : IDistributedApplicationPipeline
1819
{
1920
private readonly List<PipelineStep> _steps = [];
21+
private readonly List<Func<PipelineConfigurationContext, Task>> _configurationCallbacks = [];
2022

2123
public bool HasSteps => _steps.Count > 0;
2224

@@ -103,11 +105,21 @@ public void AddStep(PipelineStep step)
103105
_steps.Add(step);
104106
}
105107

108+
public void AddPipelineConfiguration(Func<PipelineConfigurationContext, Task> callback)
109+
{
110+
ArgumentNullException.ThrowIfNull(callback);
111+
_configurationCallbacks.Add(callback);
112+
}
113+
106114
public async Task ExecuteAsync(PipelineContext context)
107115
{
108-
var annotationSteps = await CollectStepsFromAnnotationsAsync(context).ConfigureAwait(false);
116+
var (annotationSteps, stepToResourceMap) = await CollectStepsFromAnnotationsAsync(context).ConfigureAwait(false);
109117
var allSteps = _steps.Concat(annotationSteps).ToList();
110118

119+
// Execute configuration callbacks even if there are no steps
120+
// This allows callbacks to run validation or other logic
121+
await ExecuteConfigurationCallbacksAsync(context, allSteps, stepToResourceMap).ConfigureAwait(false);
122+
111123
if (allSteps.Count == 0)
112124
{
113125
return;
@@ -182,9 +194,10 @@ void Visit(string stepName)
182194
return result;
183195
}
184196

185-
private static async Task<List<PipelineStep>> CollectStepsFromAnnotationsAsync(PipelineContext context)
197+
private static async Task<(List<PipelineStep> Steps, Dictionary<PipelineStep, IResource> StepToResourceMap)> CollectStepsFromAnnotationsAsync(PipelineContext context)
186198
{
187199
var steps = new List<PipelineStep>();
200+
var stepToResourceMap = new Dictionary<PipelineStep, IResource>();
188201

189202
foreach (var resource in context.Model.Resources)
190203
{
@@ -200,11 +213,53 @@ private static async Task<List<PipelineStep>> CollectStepsFromAnnotationsAsync(P
200213
};
201214

202215
var annotationSteps = await annotation.CreateStepsAsync(factoryContext).ConfigureAwait(false);
203-
steps.AddRange(annotationSteps);
216+
foreach (var step in annotationSteps)
217+
{
218+
steps.Add(step);
219+
stepToResourceMap[step] = resource;
220+
}
204221
}
205222
}
206223

207-
return steps;
224+
return (steps, stepToResourceMap);
225+
}
226+
227+
private async Task ExecuteConfigurationCallbacksAsync(
228+
PipelineContext pipelineContext,
229+
List<PipelineStep> allSteps,
230+
Dictionary<PipelineStep, IResource> stepToResourceMap)
231+
{
232+
// Collect callbacks from the pipeline itself
233+
var callbacks = new List<Func<PipelineConfigurationContext, Task>>();
234+
235+
callbacks.AddRange(_configurationCallbacks);
236+
237+
// Collect callbacks from resource annotations
238+
foreach (var resource in pipelineContext.Model.Resources)
239+
{
240+
var annotations = resource.Annotations.OfType<PipelineConfigurationAnnotation>();
241+
foreach (var annotation in annotations)
242+
{
243+
callbacks.Add(annotation.Callback);
244+
}
245+
}
246+
247+
// Execute all callbacks
248+
if (callbacks.Count > 0)
249+
{
250+
var configContext = new PipelineConfigurationContext
251+
{
252+
Services = pipelineContext.Services,
253+
Steps = allSteps.AsReadOnly(),
254+
Model = pipelineContext.Model,
255+
StepToResourceMap = stepToResourceMap
256+
};
257+
258+
foreach (var callback in callbacks)
259+
{
260+
await callback(configContext).ConfigureAwait(false);
261+
}
262+
}
208263
}
209264

210265
private static void ValidateSteps(IEnumerable<PipelineStep> steps)

src/Aspire.Hosting/Pipelines/IDistributedApplicationPipeline.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ void AddStep(string name,
3131
/// <param name="step">The pipeline step to add.</param>
3232
void AddStep(PipelineStep step);
3333

34+
/// <summary>
35+
/// Registers a callback to be executed during the pipeline configuration phase.
36+
/// </summary>
37+
/// <param name="callback">The callback function to execute during the configuration phase.</param>
38+
void AddPipelineConfiguration(Func<PipelineConfigurationContext, Task> callback);
39+
3440
/// <summary>
3541
/// Executes all steps in the pipeline in dependency order.
3642
/// </summary>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
#pragma warning disable ASPIREPUBLISHERS001
5+
6+
using System.Diagnostics.CodeAnalysis;
7+
using Aspire.Hosting.ApplicationModel;
8+
9+
namespace Aspire.Hosting.Pipelines;
10+
11+
/// <summary>
12+
/// An annotation that registers a callback to execute during the pipeline configuration phase,
13+
/// allowing modification of step dependencies and relationships.
14+
/// </summary>
15+
[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
16+
public class PipelineConfigurationAnnotation : IResourceAnnotation
17+
{
18+
/// <summary>
19+
/// Gets the callback function to execute during the configuration phase.
20+
/// </summary>
21+
public Func<PipelineConfigurationContext, Task> Callback { get; }
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="PipelineConfigurationAnnotation"/> class.
25+
/// </summary>
26+
/// <param name="callback">The callback function to execute during the configuration phase.</param>
27+
public PipelineConfigurationAnnotation(Func<PipelineConfigurationContext, Task> callback)
28+
{
29+
ArgumentNullException.ThrowIfNull(callback);
30+
Callback = callback;
31+
}
32+
33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="PipelineConfigurationAnnotation"/> class.
35+
/// </summary>
36+
/// <param name="callback">The synchronous callback function to execute during the configuration phase.</param>
37+
public PipelineConfigurationAnnotation(Action<PipelineConfigurationContext> callback)
38+
{
39+
ArgumentNullException.ThrowIfNull(callback);
40+
Callback = (context) =>
41+
{
42+
callback(context);
43+
return Task.CompletedTask;
44+
};
45+
}
46+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
/// Provides contextual information for pipeline configuration callbacks.
11+
/// </summary>
12+
[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
13+
public class PipelineConfigurationContext
14+
{
15+
/// <summary>
16+
/// Gets the service provider for dependency resolution.
17+
/// </summary>
18+
public required IServiceProvider Services { get; init; }
19+
20+
/// <summary>
21+
/// Gets the list of pipeline steps collected during the first pass.
22+
/// </summary>
23+
public required IReadOnlyList<PipelineStep> Steps { get; init; }
24+
25+
/// <summary>
26+
/// Gets the distributed application model containing all resources.
27+
/// </summary>
28+
public required DistributedApplicationModel Model { get; init; }
29+
30+
internal IReadOnlyDictionary<PipelineStep, IResource> StepToResourceMap { get; init; } = null!;
31+
32+
/// <summary>
33+
/// Gets all pipeline steps with the specified tag.
34+
/// </summary>
35+
/// <param name="tag">The tag to search for.</param>
36+
/// <returns>A collection of steps that have the specified tag.</returns>
37+
public IEnumerable<PipelineStep> GetSteps(string tag)
38+
{
39+
ArgumentNullException.ThrowIfNull(tag);
40+
return Steps.Where(s => s.Tags.Contains(tag));
41+
}
42+
43+
/// <summary>
44+
/// Gets all pipeline steps associated with the specified resource.
45+
/// </summary>
46+
/// <param name="resource">The resource to search for.</param>
47+
/// <returns>A collection of steps associated with the resource.</returns>
48+
public IEnumerable<PipelineStep> GetSteps(IResource resource)
49+
{
50+
ArgumentNullException.ThrowIfNull(resource);
51+
return StepToResourceMap.Where(kvp => kvp.Value == resource).Select(kvp => kvp.Key);
52+
}
53+
54+
/// <summary>
55+
/// Gets all pipeline steps with the specified tag that are associated with the specified resource.
56+
/// </summary>
57+
/// <param name="resource">The resource to search for.</param>
58+
/// <param name="tag">The tag to search for.</param>
59+
/// <returns>A collection of steps that have the specified tag and are associated with the resource.</returns>
60+
public IEnumerable<PipelineStep> GetSteps(IResource resource, string tag)
61+
{
62+
ArgumentNullException.ThrowIfNull(resource);
63+
ArgumentNullException.ThrowIfNull(tag);
64+
return GetSteps(resource).Where(s => s.Tags.Contains(tag));
65+
}
66+
}

src/Aspire.Hosting/Pipelines/PipelineStep.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ public class PipelineStep
3333
/// </summary>
3434
public List<string> RequiredBySteps { get; init; } = [];
3535

36+
/// <summary>
37+
/// Gets or initializes the list of tags that categorize this step.
38+
/// </summary>
39+
public List<string> Tags { get; init; } = [];
40+
3641
/// <summary>
3742
/// Adds a dependency on another step.
3843
/// </summary>

src/Aspire.Hosting/Pipelines/PipelineStepExtensions.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,40 @@ public static IResourceBuilder<T> WithPipelineStepFactory<T>(
8181

8282
return builder.WithAnnotation(new PipelineStepAnnotation(factory));
8383
}
84+
85+
/// <summary>
86+
/// Registers a callback to be executed during the pipeline configuration phase,
87+
/// allowing modification of step dependencies and relationships.
88+
/// </summary>
89+
/// <typeparam name="T">The type of the resource.</typeparam>
90+
/// <param name="builder">The resource builder.</param>
91+
/// <param name="callback">The callback function to execute during the configuration phase.</param>
92+
/// <returns>The resource builder for chaining.</returns>
93+
public static IResourceBuilder<T> WithPipelineConfiguration<T>(
94+
this IResourceBuilder<T> builder,
95+
Func<PipelineConfigurationContext, Task> callback) where T : IResource
96+
{
97+
ArgumentNullException.ThrowIfNull(builder);
98+
ArgumentNullException.ThrowIfNull(callback);
99+
100+
return builder.WithAnnotation(new PipelineConfigurationAnnotation(callback));
101+
}
102+
103+
/// <summary>
104+
/// Registers a callback to be executed during the pipeline configuration phase,
105+
/// allowing modification of step dependencies and relationships.
106+
/// </summary>
107+
/// <typeparam name="T">The type of the resource.</typeparam>
108+
/// <param name="builder">The resource builder.</param>
109+
/// <param name="callback">The callback function to execute during the configuration phase.</param>
110+
/// <returns>The resource builder for chaining.</returns>
111+
public static IResourceBuilder<T> WithPipelineConfiguration<T>(
112+
this IResourceBuilder<T> builder,
113+
Action<PipelineConfigurationContext> callback) where T : IResource
114+
{
115+
ArgumentNullException.ThrowIfNull(builder);
116+
ArgumentNullException.ThrowIfNull(callback);
117+
118+
return builder.WithAnnotation(new PipelineConfigurationAnnotation(callback));
119+
}
84120
}

src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs renamed to src/Aspire.Hosting/Pipelines/WellKnownPipelineTags.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@
66
namespace Aspire.Hosting.Pipelines;
77

88
/// <summary>
9-
/// Defines well-known pipeline step names used in the deployment process.
9+
/// Defines well-known pipeline tags used to categorize pipeline steps.
1010
/// </summary>
1111
[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
12-
public static class WellKnownPipelineSteps
12+
public static class WellKnownPipelineTags
1313
{
1414
/// <summary>
15-
/// The step that provisions infrastructure resources.
15+
/// Tag for steps that provision infrastructure resources.
1616
/// </summary>
1717
public const string ProvisionInfrastructure = "provision-infra";
1818

1919
/// <summary>
20-
/// The step that builds compute resources.
20+
/// Tag for steps that build compute resources.
2121
/// </summary>
2222
public const string BuildCompute = "build-compute";
2323

2424
/// <summary>
25-
/// The step that deploys to compute infrastructure.
25+
/// Tag for steps that deploy to compute infrastructure.
2626
/// </summary>
2727
public const string DeployCompute = "deploy-compute";
2828
}

0 commit comments

Comments
 (0)