Skip to content
Prev Previous commit
Next Next commit
Refactor AzurePublishingContext to be public.
  • Loading branch information
eerhardt committed Apr 4, 2025
commit 2ab85b8cd6a87cb41b101dbcb0c0016a32cc7878
32 changes: 4 additions & 28 deletions src/Aspire.Hosting.Azure/AzurePublisher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Diagnostics.CodeAnalysis;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Publishing;
using Azure.Provisioning;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -28,50 +27,27 @@ namespace Aspire.Hosting.Azure;
/// </example>
/// <seealso cref="IDistributedApplicationPublisher"/>
[Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public sealed class AzurePublisher(
internal sealed class AzurePublisher(
[ServiceKey] string name,
IOptionsMonitor<AzurePublisherOptions> options,
IOptions<AzureProvisioningOptions> provisioningOptions,
ILogger<AzurePublisher> logger) : IDistributedApplicationPublisher
{
private AzureProvisioningOptions ProvisioningOptions => provisioningOptions.Value;

/// <summary>
/// Publishes the specified distributed application model to Azure using Bicep templates.
/// </summary>
/// <param name="model">The distributed application model to publish.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task that represents the asynchronous publish operation.</returns>
public Task PublishAsync(DistributedApplicationModel model, CancellationToken cancellationToken)
public async Task PublishAsync(DistributedApplicationModel model, CancellationToken cancellationToken)
{
var publisherOptions = options.Get(name);

var outputDirectory = new DirectoryInfo(publisherOptions.OutputPath!);
outputDirectory.Create();

var context = new AzurePublishingContext(publisherOptions, logger);

context.WriteModelAsync(model).ConfigureAwait(false);

SaveToDiskAsync(outputDirectory.FullName, context.Infra).ConfigureAwait(false);

return Task.CompletedTask;
}

/// <summary>
/// Saves the compiled Bicep template to disk.
/// </summary>
/// <param name="outputDirectoryPath">The path to the output directory where the Bicep template will be saved.</param>
/// <param name="infrastructure">The infrastructure object containing the compiled Bicep template.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
public async Task SaveToDiskAsync(string outputDirectoryPath, Infrastructure infrastructure)
{
var plan = infrastructure.Build(ProvisioningOptions.ProvisioningBuildOptions);
var compiledBicep = plan.Compile().First();

logger.LogDebug("Writing Bicep module {BicepName}.bicep to {TargetPath}", infrastructure.BicepName, outputDirectoryPath);
var context = new AzurePublishingContext(publisherOptions, provisioningOptions.Value, logger);

var bicepPath = Path.Combine(outputDirectoryPath, $"{infrastructure.BicepName}.bicep");
await File.WriteAllTextAsync(bicepPath, compiledBicep.Value).ConfigureAwait(false);
await context.WriteModelAsync(model, cancellationToken).ConfigureAwait(false);
}
}
60 changes: 44 additions & 16 deletions src/Aspire.Hosting.Azure/AzurePublishingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,37 @@
namespace Aspire.Hosting.Azure;

/// <summary>
/// Represents a context for publishing Azure Resource Manager (ARM) templates for a distributed application.
/// Represents a context for publishing Azure bicep templates for a distributed application.
/// </summary>
/// <remarks>
/// This context facilitates the generation of ARM templates using the provided application model,
/// This context facilitates the generation of bicep templates using the provided application model,
/// publisher options, and execution context. It handles resource configuration and ensures
/// that the ARM template is created in the specified output path.
/// that the bicep template is created in the specified output path.
/// </remarks>
[Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
internal sealed class AzurePublishingContext(
public sealed class AzurePublishingContext(
AzurePublisherOptions publisherOptions,
AzureProvisioningOptions provisioningOptions,
ILogger logger)
{
private ILogger Logger => logger;
private AzurePublisherOptions PublisherOptions => publisherOptions;

public Infrastructure Infra = new()
/// <summary>
/// Gets the main.bicep infrastructure for the distributed application.
/// </summary>
public Infrastructure MainInfrastructure = new()
{
TargetScope = DeploymentScope.Subscription
};

internal async Task WriteModelAsync(DistributedApplicationModel model)
/// <summary>
/// Writes the specified distributed application model to the output path using Bicep templates.
/// </summary>
/// <param name="model">The distributed application model to write to the output path.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task that represents the async operation.</returns>
public async Task WriteModelAsync(DistributedApplicationModel model, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(model);
ArgumentNullException.ThrowIfNull(PublisherOptions.OutputPath);
Expand All @@ -43,10 +53,12 @@ internal async Task WriteModelAsync(DistributedApplicationModel model)
return;
}

await WriteAzureArtifactsOutputAsync(model).ConfigureAwait(false);
await WriteAzureArtifactsOutputAsync(model, cancellationToken).ConfigureAwait(false);

await SaveToDiskAsync(PublisherOptions.OutputPath).ConfigureAwait(false);
}

private Task WriteAzureArtifactsOutputAsync(DistributedApplicationModel model)
private Task WriteAzureArtifactsOutputAsync(DistributedApplicationModel model, CancellationToken _)
{
var outputDirectory = new DirectoryInfo(PublisherOptions.OutputPath!);
if (!outputDirectory.Exists)
Expand All @@ -55,13 +67,13 @@ private Task WriteAzureArtifactsOutputAsync(DistributedApplicationModel model)
}

var environmentParam = new ProvisioningParameter("environmentName", typeof(string));
Infra.Add(environmentParam);
MainInfrastructure.Add(environmentParam);

var locationParam = new ProvisioningParameter("location", typeof(string));
Infra.Add(locationParam);
MainInfrastructure.Add(locationParam);

var principalId = new ProvisioningParameter("principalId", typeof(string));
Infra.Add(principalId);
MainInfrastructure.Add(principalId);

var tags = new ProvisioningVariable("tags", typeof(object))
{
Expand Down Expand Up @@ -223,15 +235,15 @@ static BicepValue<string> ResolveValue(object val)

foreach (var (_, pp) in parameterMap)
{
Infra.Add(pp);
MainInfrastructure.Add(pp);
}

Infra.Add(tags);
Infra.Add(rg);
MainInfrastructure.Add(tags);
MainInfrastructure.Add(rg);

foreach (var (_, module) in moduleMap)
{
Infra.Add(module);
MainInfrastructure.Add(module);
}

foreach (var (_, output) in outputs)
Expand All @@ -245,7 +257,7 @@ static BicepValue<string> ResolveValue(object val)
Value = GetOutputs(module, output.Name)
};

Infra.Add(bicepOutput);
MainInfrastructure.Add(bicepOutput);
}

return Task.CompletedTask;
Expand All @@ -271,4 +283,20 @@ private static void Visit(object? value, Action<object> visitor, HashSet<object>
}
}
}

/// <summary>
/// Saves the compiled Bicep template to disk.
/// </summary>
/// <param name="outputDirectoryPath">The path to the output directory where the Bicep template will be saved.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
private async Task SaveToDiskAsync(string outputDirectoryPath)
{
var plan = MainInfrastructure.Build(provisioningOptions.ProvisioningBuildOptions);
var compiledBicep = plan.Compile().First();

logger.LogDebug("Writing Bicep module {BicepName}.bicep to {TargetPath}", MainInfrastructure.BicepName, outputDirectoryPath);

var bicepPath = Path.Combine(outputDirectoryPath, $"{MainInfrastructure.BicepName}.bicep");
await File.WriteAllTextAsync(bicepPath, compiledBicep.Value).ConfigureAwait(false);
}
}
30 changes: 23 additions & 7 deletions tests/Aspire.Hosting.Azure.Tests/AzurePublisherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ namespace Aspire.Hosting.Azure.Tests;

public class AzurePublisherTests(ITestOutputHelper output)
{
[Fact]
public async Task PublishAsync_GeneratesMainBicep()
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task PublishAsync_GeneratesMainBicep(bool useContext)
{
using var tempDirectory = new TempDirectory();
using var tempDir = new TempDirectory();
Expand Down Expand Up @@ -72,12 +74,26 @@ public async Task PublishAsync_GeneratesMainBicep()

await ExecuteBeforeStartHooksAsync(app, default);

var publisher = new AzurePublisher("azure",
options,
provisionerOptions,
NullLogger<AzurePublisher>.Instance);
if (useContext)
{
// tests the public AzurePublishingContext API
var context = new AzurePublishingContext(
options.CurrentValue,
provisionerOptions.Value,
NullLogger<AzurePublishingContext>.Instance);

await publisher.PublishAsync(model, default);
await context.WriteModelAsync(model, default);
}
else
{
// tests via the internal Publisher object
var publisher = new AzurePublisher("azure",
options,
provisionerOptions,
NullLogger<AzurePublisher>.Instance);

await publisher.PublishAsync(model, default);
}

Assert.True(File.Exists(Path.Combine(tempDir.Path, "main.bicep")));

Expand Down