Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions src/Aspire.Hosting.Azure/Provisioning/BicepUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,34 @@ namespace Aspire.Hosting.Azure.Provisioning;
/// </summary>
internal static class BicepUtilities
{
// Known values since they will be filled in by the provisioner
private static readonly string[] s_knownParameterNames =
[
AzureBicepResource.KnownParameters.PrincipalName,
AzureBicepResource.KnownParameters.PrincipalId,
AzureBicepResource.KnownParameters.PrincipalType,
AzureBicepResource.KnownParameters.UserPrincipalId,
AzureBicepResource.KnownParameters.Location,
];

/// <summary>
/// Converts the parameters to a JSON object compatible with the ARM template.
/// </summary>
public static async Task SetParametersAsync(JsonObject parameters, AzureBicepResource resource, CancellationToken cancellationToken = default)
public static async Task SetParametersAsync(JsonObject parameters, AzureBicepResource resource, bool skipKnownValues = false, CancellationToken cancellationToken = default)
{
// Convert the parameters to a JSON object
foreach (var parameter in resource.Parameters)
{
// Execute parameter values which are deferred.
var parameterValue = parameter.Value is Func<object?> f ? f() : parameter.Value;

// Skip known parameters with 'null' values, like PrincipalType and PrincipalId, since they are filled in by the provisioner
// and are not available at this time. If we don't do this, the "roles" resources will be re-deployed every run.
if (skipKnownValues && s_knownParameterNames.Contains(parameter.Key) && parameterValue is null)
{
continue;
}

parameters[parameter.Key] = new JsonObject()
{
["value"] = parameterValue switch
Expand Down Expand Up @@ -109,7 +126,7 @@ public static string GetChecksum(AzureBicepResource resource, JsonObject paramet
_ = resource.GetBicepTemplateString();

// Now overwrite with live object values skipping known values.
await SetParametersAsync(parameters, resource, cancellationToken: cancellationToken).ConfigureAwait(false);
await SetParametersAsync(parameters, resource, skipKnownValues: true, cancellationToken: cancellationToken).ConfigureAwait(false);
if (scope is not null)
{
await SetScopeAsync(scope, resource, cancellationToken).ConfigureAwait(false);
Expand All @@ -130,4 +147,4 @@ public static string GetChecksum(AzureBicepResource resource, JsonObject paramet
(resource.TryGetLastAnnotation<ExistingAzureResourceAnnotation>(out var existingResource) ?
existingResource.ResourceGroup :
null);
}
}
40 changes: 39 additions & 1 deletion tests/Aspire.Hosting.Azure.Tests/BicepUtilitiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -428,11 +428,49 @@ public async Task GetCurrentChecksumAsync_ReturnsValidChecksumForValidParameters
Assert.NotEmpty(result);
}

/// <summary>
/// Ensures that known parameters are not overwritten when calculating the checksum.
/// This is important because if these known parameters are overwritten, it means the "roles"
/// resources will be redeployed every time the app is run.
/// </summary>
[Fact]
public async Task GetCurrentChecksumAsync_DoesNotOverwriteKnownParameters()
{
// Arrange
using var builder = TestDistributedApplicationBuilder.Create();
var bicep = builder.AddBicepTemplateString("test", "param name string").Resource;
bicep.Parameters[AzureBicepResource.KnownParameters.PrincipalType] = null;
bicep.Parameters[AzureBicepResource.KnownParameters.PrincipalId] = null;

var parameters = new JsonObject
{
[AzureBicepResource.KnownParameters.PrincipalType] = new JsonObject { ["value"] = "User" },
[AzureBicepResource.KnownParameters.PrincipalId] = new JsonObject { ["value"] = "1234" },
};

var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
["Parameters"] = parameters.ToJsonString()
});
var config = configurationBuilder.Build();

// Act
var result = await BicepUtilities.GetCurrentChecksumAsync(bicep, config);

// Assert
Assert.NotNull(result);

// verify the checksum is the same as using the config parameters directly
var expected = BicepUtilities.GetChecksum(bicep, parameters, scope: null);
Assert.Equal(expected, result);
}

private sealed class ResourceWithConnectionString(string name, string connectionString) :
Resource(name),
IResourceWithConnectionString
{
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create($"{connectionString}");
}
}
}