Skip to content

Commit 8d9c32f

Browse files
Refactor AzurePublisher to enable usage in downstream publishers (#8507)
Few refactoring changes to AzurePublisher to enable usage in downstream publishers: Refactor the logic in AzurePublisher to a new public AzurePublishingContext class. refactor code in AzurePublisher.PublishAsync to AzurePublishingContext and capture bicep compilation data that is thrown away in the existing AzurePublisher.PublishAsync method * Restructure AzurePublisher and add AzurePublishingContext * Few more edits and add an playgroud app for AzurePublisher * Move SaveToDisk to Publisher * Edit Playground aspire starter app to use AspireProjectOrPackageReference and call AddAzurePublisher * Remove AzurePublisher playground app * Refactor AzurePublishingContext to be public. * Fix up .sln file --------- Co-authored-by: Eric Erhardt <[email protected]>
1 parent 6656af4 commit 8d9c32f

File tree

3 files changed

+350
-271
lines changed

3 files changed

+350
-271
lines changed

src/Aspire.Hosting.Azure/AzurePublisher.cs

Lines changed: 25 additions & 264 deletions
Original file line numberDiff line numberDiff line change
@@ -4,289 +4,50 @@
44
using System.Diagnostics.CodeAnalysis;
55
using Aspire.Hosting.ApplicationModel;
66
using Aspire.Hosting.Publishing;
7-
using Azure.Provisioning;
8-
using Azure.Provisioning.Expressions;
9-
using Azure.Provisioning.Primitives;
10-
using Azure.Provisioning.Resources;
117
using Microsoft.Extensions.DependencyInjection;
128
using Microsoft.Extensions.Logging;
139
using Microsoft.Extensions.Options;
1410

1511
namespace Aspire.Hosting.Azure;
1612

13+
/// <summary>
14+
/// Represents a publisher for deploying distributed application models to Azure using Bicep templates.
15+
/// </summary>
16+
/// <remarks>
17+
/// This class is responsible for processing a distributed application model, generating Bicep templates,
18+
/// and configuring Azure infrastructure for deployment. It supports parameter resolution, resource grouping,
19+
/// and output propagation for Azure resources.
20+
/// </remarks>
21+
/// <example>
22+
/// Example usage:
23+
/// <code>
24+
/// var publisher = new AzurePublisher("myPublisher", optionsMonitor, provisioningOptions, logger);
25+
/// await publisher.PublishAsync(model, cancellationToken);
26+
/// </code>
27+
/// </example>
28+
/// <seealso cref="IDistributedApplicationPublisher"/>
1729
[Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
1830
internal sealed class AzurePublisher(
1931
[ServiceKey] string name,
2032
IOptionsMonitor<AzurePublisherOptions> options,
2133
IOptions<AzureProvisioningOptions> provisioningOptions,
2234
ILogger<AzurePublisher> logger) : IDistributedApplicationPublisher
2335
{
24-
private AzureProvisioningOptions ProvisioningOptions => provisioningOptions.Value;
25-
26-
public Task PublishAsync(DistributedApplicationModel model, CancellationToken cancellationToken)
36+
/// <summary>
37+
/// Publishes the specified distributed application model to Azure using Bicep templates.
38+
/// </summary>
39+
/// <param name="model">The distributed application model to publish.</param>
40+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
41+
/// <returns>A task that represents the asynchronous publish operation.</returns>
42+
public async Task PublishAsync(DistributedApplicationModel model, CancellationToken cancellationToken)
2743
{
2844
var publisherOptions = options.Get(name);
2945

30-
var infra = new Infrastructure
31-
{
32-
TargetScope = DeploymentScope.Subscription
33-
};
34-
35-
var environmentParam = new ProvisioningParameter("environmentName", typeof(string));
36-
infra.Add(environmentParam);
37-
38-
var locationParam = new ProvisioningParameter("location", typeof(string));
39-
infra.Add(locationParam);
40-
41-
var principalId = new ProvisioningParameter("principalId", typeof(string));
42-
infra.Add(principalId);
43-
44-
var tags = new ProvisioningVariable("tags", typeof(object))
45-
{
46-
Value = new BicepDictionary<string>
47-
{
48-
["aspire-env-name"] = environmentParam
49-
}
50-
};
51-
52-
// REVIEW: Do we want people to be able to change this
53-
var rg = new ResourceGroup("rg")
54-
{
55-
Name = BicepFunction.Interpolate($"rg-{environmentParam}"),
56-
Location = locationParam,
57-
Tags = tags
58-
};
59-
6046
var outputDirectory = new DirectoryInfo(publisherOptions.OutputPath!);
61-
6247
outputDirectory.Create();
6348

64-
// Process the resources in the model and create a module for each one
65-
var moduleMap = new Dictionary<AzureBicepResource, ModuleImport>();
66-
67-
foreach (var resource in model.Resources.OfType<AzureBicepResource>())
68-
{
69-
var file = resource.GetBicepTemplateFile();
70-
71-
var moduleDirectory = outputDirectory.CreateSubdirectory(resource.Name);
72-
73-
var modulePath = Path.Combine(moduleDirectory.FullName, $"{resource.Name}.bicep");
74-
75-
File.Copy(file.Path, modulePath, true);
76-
77-
var identifier = Infrastructure.NormalizeBicepIdentifier(resource.Name);
78-
79-
var module = new ModuleImport(identifier, $"{resource.Name}/{resource.Name}.bicep")
80-
{
81-
Name = resource.Name
82-
};
83-
84-
moduleMap[resource] = module;
85-
}
86-
87-
// Resolve parameters *after* writing the modules to disk
88-
// this is because some parameters are added in ConfigureInfrastructure callbacks
89-
var parameterMap = new Dictionary<ParameterResource, ProvisioningParameter>();
90-
91-
foreach (var resource in model.Resources.OfType<AzureBicepResource>())
92-
{
93-
foreach (var parameter in resource.Parameters)
94-
{
95-
Visit(parameter.Value, v =>
96-
{
97-
if (v is ParameterResource p && !parameterMap.ContainsKey(p))
98-
{
99-
var pid = Infrastructure.NormalizeBicepIdentifier(p.Name);
100-
101-
var pp = new ProvisioningParameter(pid, typeof(string))
102-
{
103-
IsSecure = p.Secret
104-
};
105-
106-
if (!p.Secret && p.Default is not null)
107-
{
108-
pp.Value = p.Value;
109-
}
110-
111-
// Map the parameter to the Bicep parameter
112-
parameterMap[p] = pp;
113-
}
114-
});
115-
}
116-
}
117-
118-
static BicepValue<string> GetOutputs(ModuleImport module, string outputName) =>
119-
new MemberExpression(new MemberExpression(new IdentifierExpression(module.BicepIdentifier), "outputs"), outputName);
120-
121-
BicepFormatString EvalExpr(ReferenceExpression expr)
122-
{
123-
var args = new object[expr.ValueProviders.Count];
124-
125-
for (var i = 0; i < expr.ValueProviders.Count; i++)
126-
{
127-
args[i] = Eval(expr.ValueProviders[i]);
128-
}
129-
130-
return new BicepFormatString(expr.Format, args);
131-
}
132-
133-
object Eval(object? value) => value switch
134-
{
135-
BicepOutputReference b => GetOutputs(moduleMap[b.Resource], b.Name),
136-
ParameterResource p => parameterMap[p],
137-
ConnectionStringReference r => Eval(r.Resource.ConnectionStringExpression),
138-
IResourceWithConnectionString cs => Eval(cs.ConnectionStringExpression),
139-
ReferenceExpression re => EvalExpr(re),
140-
string s => s,
141-
_ => ""
142-
};
143-
144-
static BicepValue<string> ResolveValue(object val)
145-
{
146-
return val switch
147-
{
148-
BicepValue<string> s => s,
149-
string s => s,
150-
ProvisioningParameter p => p,
151-
BicepFormatString fs => BicepFunction2.Interpolate(fs),
152-
_ => throw new NotSupportedException("Unsupported value type " + val.GetType())
153-
};
154-
}
155-
156-
foreach (var resource in model.Resources.OfType<AzureBicepResource>())
157-
{
158-
BicepValue<string> scope = resource.Scope?.ResourceGroup switch
159-
{
160-
// resourceGroup(rgName)
161-
string rgName => new FunctionCallExpression(new IdentifierExpression("resourceGroup"), new StringLiteralExpression(rgName)),
162-
ParameterResource p => parameterMap[p],
163-
_ => new IdentifierExpression(rg.BicepIdentifier)
164-
};
165-
166-
var module = moduleMap[resource];
167-
module.Scope = scope;
168-
module.Parameters.Add("location", locationParam);
169-
170-
foreach (var parameter in resource.Parameters)
171-
{
172-
// TODO: There are a set of known parameter names that we may not be able to resolve.
173-
// This is from earlier versions of aspire where infra was split across
174-
// azd and aspire. Once the infra moves to aspire, we can throw for
175-
// unresolved "known parameters".
176-
177-
if (parameter.Key == AzureBicepResource.KnownParameters.UserPrincipalId && parameter.Value is null)
178-
{
179-
module.Parameters.Add(parameter.Key, principalId);
180-
continue;
181-
}
182-
183-
var value = ResolveValue(Eval(parameter.Value));
184-
185-
module.Parameters.Add(parameter.Key, value);
186-
}
187-
}
188-
189-
var outputs = new Dictionary<string, BicepOutputReference>();
190-
191-
// Now find all resources that have deployment targets that are bicep modules
192-
foreach (var resource in model.Resources)
193-
{
194-
if (resource.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var targetAnnotation) &&
195-
targetAnnotation.DeploymentTarget is AzureBicepResource br)
196-
{
197-
var moduleDirectory = outputDirectory.CreateSubdirectory(resource.Name);
198-
199-
var modulePath = Path.Combine(moduleDirectory.FullName, $"{resource.Name}.bicep");
200-
201-
var file = br.GetBicepTemplateFile();
202-
203-
File.Copy(file.Path, modulePath, true);
204-
205-
// TODO: Resolve parameters for the module and
206-
// handle flowing outputs from other modules
207-
208-
foreach (var parameter in br.Parameters)
209-
{
210-
Visit(parameter.Value, v =>
211-
{
212-
if (v is BicepOutputReference bo)
213-
{
214-
// Any bicep output reference needs to be propagated to the top level
215-
outputs[bo.ValueExpression] = bo;
216-
}
217-
});
218-
}
219-
}
220-
}
221-
222-
// Add parameters to the infrastructure
223-
foreach (var (_, pp) in parameterMap)
224-
{
225-
infra.Add(pp);
226-
}
227-
228-
// Add the parameters to the infrastructure
229-
infra.Add(tags);
230-
231-
// Add the resource group to the infrastructure
232-
infra.Add(rg);
233-
234-
// Add the modules to the infrastructure
235-
foreach (var (_, module) in moduleMap)
236-
{
237-
// Add the module to the infrastructure
238-
infra.Add(module);
239-
}
240-
241-
// Add the outputs to the infrastructure
242-
foreach (var (_, output) in outputs)
243-
{
244-
var module = moduleMap[output.Resource];
245-
246-
var identifier = Infrastructure.NormalizeBicepIdentifier($"{output.Resource.Name}_{output.Name}");
247-
248-
var bicepOutput = new ProvisioningOutput(identifier, typeof(string))
249-
{
250-
Value = GetOutputs(module, output.Name)
251-
};
252-
253-
infra.Add(bicepOutput);
254-
}
255-
256-
SaveToDisk(outputDirectory.FullName, infra);
257-
258-
return Task.CompletedTask;
259-
}
260-
261-
private static void Visit(object? value, Action<object> visitor) =>
262-
Visit(value, visitor, []);
263-
264-
private static void Visit(object? value, Action<object> visitor, HashSet<object> visited)
265-
{
266-
if (value is null || !visited.Add(value))
267-
{
268-
return;
269-
}
270-
271-
visitor(value);
272-
273-
if (value is IValueWithReferences vwr)
274-
{
275-
foreach (var reference in vwr.References)
276-
{
277-
Visit(reference, visitor, visited);
278-
}
279-
}
280-
}
281-
282-
private void SaveToDisk(string outputDirectoryPath, Infrastructure infrastructure)
283-
{
284-
var plan = infrastructure.Build(ProvisioningOptions.ProvisioningBuildOptions);
285-
var compiledBicep = plan.Compile().First();
286-
287-
logger.LogDebug("Writing Bicep module {BicepName}.bicep to {TargetPath}", infrastructure.BicepName, outputDirectoryPath);
49+
var context = new AzurePublishingContext(publisherOptions, provisioningOptions.Value, logger);
28850

289-
var bicepPath = Path.Combine(outputDirectoryPath, $"{infrastructure.BicepName}.bicep");
290-
File.WriteAllText(bicepPath, compiledBicep.Value);
51+
await context.WriteModelAsync(model, cancellationToken).ConfigureAwait(false);
29152
}
29253
}

0 commit comments

Comments
 (0)