|
4 | 4 | using System.Diagnostics.CodeAnalysis; |
5 | 5 | using Aspire.Hosting.ApplicationModel; |
6 | 6 | using Aspire.Hosting.Publishing; |
7 | | -using Azure.Provisioning; |
8 | | -using Azure.Provisioning.Expressions; |
9 | | -using Azure.Provisioning.Primitives; |
10 | | -using Azure.Provisioning.Resources; |
11 | 7 | using Microsoft.Extensions.DependencyInjection; |
12 | 8 | using Microsoft.Extensions.Logging; |
13 | 9 | using Microsoft.Extensions.Options; |
14 | 10 |
|
15 | 11 | namespace Aspire.Hosting.Azure; |
16 | 12 |
|
| 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"/> |
17 | 29 | [Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")] |
18 | 30 | internal sealed class AzurePublisher( |
19 | 31 | [ServiceKey] string name, |
20 | 32 | IOptionsMonitor<AzurePublisherOptions> options, |
21 | 33 | IOptions<AzureProvisioningOptions> provisioningOptions, |
22 | 34 | ILogger<AzurePublisher> logger) : IDistributedApplicationPublisher |
23 | 35 | { |
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) |
27 | 43 | { |
28 | 44 | var publisherOptions = options.Get(name); |
29 | 45 |
|
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 | | - |
60 | 46 | var outputDirectory = new DirectoryInfo(publisherOptions.OutputPath!); |
61 | | - |
62 | 47 | outputDirectory.Create(); |
63 | 48 |
|
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); |
288 | 50 |
|
289 | | - var bicepPath = Path.Combine(outputDirectoryPath, $"{infrastructure.BicepName}.bicep"); |
290 | | - File.WriteAllText(bicepPath, compiledBicep.Value); |
| 51 | + await context.WriteModelAsync(model, cancellationToken).ConfigureAwait(false); |
291 | 52 | } |
292 | 53 | } |
0 commit comments