// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#pragma warning disable ASPIREPIPELINES002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
using System.IO.Hashing;
using System.Text;
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Pipelines;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Aspire.Hosting.Azure.Provisioning;
/// <summary>
/// Utility methods for working with Bicep resources.
/// </summary>
internal static class BicepUtilities
{
internal const string DeploymentStateIdKey = "Id";
internal const string DeploymentStateParametersKey = "Parameters";
internal const string DeploymentStateOutputsKey = "Outputs";
internal const string DeploymentStateScopeKey = "Scope";
internal const string DeploymentStateChecksumKey = "CheckSum";
// 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)
{
scope.Clear();
// Resolve the scope from the AzureBicepResource if it has already been set
// via the ConfigureInfrastructure callback. If not, fallback to the ExistingAzureResourceAnnotation.
var targetScope = GetExistingResourceScope(resource);
if (targetScope is null)
{
scope["resourceGroup"] = null;
return;
}
if (targetScope.HasResourceGroup)
{
await SetScopeValueAsync(scope, "resourceGroup", targetScope.ResourceGroup, cancellationToken).ConfigureAwait(false);
}
await SetScopeValueAsync(scope, "subscription", targetScope.Subscription, cancellationToken).ConfigureAwait(false);
if (targetScope.IsTenantScope)
{
scope["tenant"] = "current";
}
}
/// <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[DeploymentStateParametersKey] is not string jsonString)
{
return null;
}
try
{
var parameters = JsonNode.Parse(jsonString)?.AsObject();
var scope = section[DeploymentStateScopeKey] is string scopeString
? JsonNode.Parse(scopeString)?.AsObject()
: GetExistingResourceScope(resource) is not null
? new JsonObject()
: 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;
}
}
/// <summary>
/// Gets the current checksum for a Bicep resource from deployment state.
/// </summary>
public static async ValueTask<string?> GetCurrentChecksumAsync(AzureBicepResource resource, DeploymentStateSection section, ILogger logger, CancellationToken cancellationToken = default)
{
if (section.Data[DeploymentStateParametersKey]?.GetValue<string>() is not { Length: > 0 } jsonString)
{
return null;
}
try
{
var parameters = JsonNode.Parse(jsonString)?.AsObject();
var scope = section.Data[DeploymentStateScopeKey]?.GetValue<string>() is { Length: > 0 } scopeString
? JsonNode.Parse(scopeString)?.AsObject()
: GetExistingResourceScope(resource) is not null
? new JsonObject()
: null;
if (parameters is null)
{
return null;
}
_ = resource.GetBicepTemplateString();
await SetParametersAsync(parameters, resource, skipKnownValues: true, cancellationToken: cancellationToken).ConfigureAwait(false);
if (scope is not null)
{
await SetScopeAsync(scope, resource, cancellationToken).ConfigureAwait(false);
}
return GetChecksum(resource, parameters, scope);
}
catch (Exception ex)
{
logger.LogDebug(ex, "Unable to compute current checksum for resource {ResourceName}.", resource.Name);
return null;
}
}
internal static AzureBicepResourceScope? GetExistingResourceScope(AzureBicepResource resource)
{
if (resource.Scope is not null)
{
return resource.Scope;
}
return resource.TryGetLastAnnotation<ExistingAzureResourceAnnotation>(out var existingResource)
? AzureBicepResourceScope.FromExistingResourceAnnotation(existingResource)
: null;
}
private static async Task SetScopeValueAsync(JsonObject scope, string propertyName, object? value, CancellationToken cancellationToken)
{
if (value is null)
{
return;
}
scope[propertyName] = value switch
{
string s => s,
IValueProvider v => await v.GetValueAsync(cancellationToken).ConfigureAwait(false),
_ => throw new NotSupportedException($"The scope {propertyName} value type {value.GetType()} is not supported.")
};
}
}