diff --git a/src/Aspire.Hosting.Azure/Provisioning/BicepUtilities.cs b/src/Aspire.Hosting.Azure/Provisioning/BicepUtilities.cs index 0e263b7f1a3..38c43fea9ed 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/BicepUtilities.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/BicepUtilities.cs @@ -14,10 +14,20 @@ namespace Aspire.Hosting.Azure.Provisioning; /// 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, + ]; + /// /// Converts the parameters to a JSON object compatible with the ARM template. /// - 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) @@ -25,6 +35,13 @@ public static async Task SetParametersAsync(JsonObject parameters, AzureBicepRes // Execute parameter values which are deferred. var parameterValue = parameter.Value is Func 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 @@ -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); @@ -130,4 +147,4 @@ public static string GetChecksum(AzureBicepResource resource, JsonObject paramet (resource.TryGetLastAnnotation(out var existingResource) ? existingResource.ResourceGroup : null); -} \ No newline at end of file +} diff --git a/tests/Aspire.Hosting.Azure.Tests/BicepUtilitiesTests.cs b/tests/Aspire.Hosting.Azure.Tests/BicepUtilitiesTests.cs index 86f1a60fbb1..35a862f1079 100644 --- a/tests/Aspire.Hosting.Azure.Tests/BicepUtilitiesTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/BicepUtilitiesTests.cs @@ -428,6 +428,44 @@ public async Task GetCurrentChecksumAsync_ReturnsValidChecksumForValidParameters Assert.NotEmpty(result); } + /// + /// 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. + /// + [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 + { + ["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 @@ -435,4 +473,4 @@ private sealed class ResourceWithConnectionString(string name, string connection public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"{connectionString}"); } -} \ No newline at end of file +}