| File: Provisioning\BicepUtilities.cs | Web Access |
| Project: src\src\Aspire.Hosting.Azure\Aspire.Hosting.Azure.csproj (Aspire.Hosting.Azure) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO.Hashing; using System.Text; using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; using Microsoft.Extensions.Configuration; namespace Aspire.Hosting.Azure.Provisioning; /// <summary> /// Utility methods for working with Bicep resources. /// </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, 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 { string s => s, IEnumerable<string> s => new JsonArray(s.Select(s => JsonValue.Create(s)).ToArray()), int i => i, bool b => b, Guid g => g.ToString(), JsonNode node => node, IValueProvider v => await v.GetValueAsync(cancellationToken).ConfigureAwait(false), null => null, _ => throw new NotSupportedException($"The parameter value type {parameterValue.GetType()} is not supported.") } }; } } /// <summary> /// Sets the scope information for a Bicep resource. /// </summary> public static async Task SetScopeAsync(JsonObject scope, AzureBicepResource resource, CancellationToken cancellationToken = default) { // Resolve the scope from the AzureBicepResource if it has already been set // via the ConfigureInfrastructure callback. If not, fallback to the ExistingAzureResourceAnnotation. var targetScope = GetExistingResourceGroup(resource); scope["resourceGroup"] = targetScope switch { string s => s, IValueProvider v => await v.GetValueAsync(cancellationToken).ConfigureAwait(false), null => null, _ => throw new NotSupportedException($"The scope value type {targetScope.GetType()} is not supported.") }; } /// <summary> /// Gets the checksum for a Bicep resource configuration. /// </summary> public static string GetChecksum(AzureBicepResource resource, JsonObject parameters, JsonObject? scope) { // TODO: PERF Inefficient // Combine the parameter values with the bicep template to create a unique value var input = parameters.ToJsonString() + resource.GetBicepTemplateString(); if (scope is not null) { input += scope.ToJsonString(); } // Hash the contents var hashedContents = Crc32.Hash(Encoding.UTF8.GetBytes(input)); // Convert the hash to a string return Convert.ToHexString(hashedContents).ToLowerInvariant(); } /// <summary> /// Gets the current checksum for a Bicep resource from configuration. /// </summary> public static async ValueTask<string?> GetCurrentChecksumAsync(AzureBicepResource resource, IConfiguration section, CancellationToken cancellationToken = default) { // Fill in parameters from configuration if (section["Parameters"] is not string jsonString) { return null; } try { var parameters = JsonNode.Parse(jsonString)?.AsObject(); var scope = section["Scope"] is string scopeString ? JsonNode.Parse(scopeString)?.AsObject() : null; if (parameters is null) { return null; } // Force evaluation of the Bicep template to ensure parameters are expanded _ = resource.GetBicepTemplateString(); // Now overwrite with live object values skipping known values. await SetParametersAsync(parameters, resource, skipKnownValues: true, cancellationToken: cancellationToken).ConfigureAwait(false); if (scope is not null) { await SetScopeAsync(scope, resource, cancellationToken).ConfigureAwait(false); } // Get the checksum of the new values return GetChecksum(resource, parameters, scope); } catch { // Unable to parse the JSON, to treat it as not existing return null; } } internal static object? GetExistingResourceGroup(AzureBicepResource resource) => resource.Scope?.ResourceGroup ?? (resource.TryGetLastAnnotation<ExistingAzureResourceAnnotation>(out var existingResource) ? existingResource.ResourceGroup : null); }