src/Bicep.Core/Utils/TemplateEvaluator.cs (283 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections.Immutable; using System.Diagnostics; using System.Text.RegularExpressions; using Azure.Deployments.Core; using Azure.Deployments.Core.Configuration; using Azure.Deployments.Core.Definitions; using Azure.Deployments.Core.Definitions.Extensibility; using Azure.Deployments.Core.Definitions.Schema; using Azure.Deployments.Core.Diagnostics; using Azure.Deployments.Core.ErrorResponses; using Azure.Deployments.Expression.Engines; using Azure.Deployments.Expression.Expressions; using Azure.Deployments.Expression.Intermediate; using Azure.Deployments.Expression.Intermediate.Extensions; using Azure.Deployments.Templates.Engines; using Azure.Deployments.Templates.Expressions; using Azure.Deployments.Templates.Expressions.PartialEvaluation; using Bicep.Core.Emit; using Bicep.Core.Features; using Microsoft.WindowsAzure.ResourceStack.Common.Collections; using Microsoft.WindowsAzure.ResourceStack.Common.Extensions; using Newtonsoft.Json.Linq; using FunctionExpression = Azure.Deployments.Expression.Expressions.FunctionExpression; using IntermediateEvaluationContext = Azure.Deployments.Expression.Intermediate.ExpressionEvaluationContext; namespace Bicep.Core.Utils { public partial class TemplateEvaluator { private class NoOpTemplateMetricRecorder : ITemplateMetricsRecorder { public static readonly NoOpTemplateMetricRecorder Instance = new(); public void Record(MetricDatum metricDatum) { } } private class TemplateEvaluationContext : IEvaluationContext { private readonly IEvaluationContext context; private readonly OrdinalInsensitiveDictionary<TemplateResource> resourceLookup; private readonly EvaluationConfiguration config; private TemplateEvaluationContext(IEvaluationContext context, ExpressionScope scope, OrdinalInsensitiveDictionary<TemplateResource> resourceLookup, EvaluationConfiguration config) { this.context = context; this.Scope = scope; this.resourceLookup = resourceLookup; this.config = config; } public static TemplateEvaluationContext Create(Template template, OrdinalInsensitiveDictionary<TemplateResource> resourceLookup, EvaluationConfiguration config) { var context = TemplateEngine.GetExpressionEvaluationContext(config.ManagementGroup, config.SubscriptionId, config.ResourceGroup, template, NoOpTemplateMetricRecorder.Instance); return new TemplateEvaluationContext(context, context.Scope, resourceLookup, config); } public bool IsShortCircuitAllowed => this.context.IsShortCircuitAllowed; public ExpressionScope Scope { get; } public bool AllowInvalidProperty(Exception exception, FunctionExpression functionExpression, FunctionArgument[] functionParametersValues, JToken[] selectedProperties) => this.context.AllowInvalidProperty(exception, functionExpression, functionParametersValues, selectedProperties); public JToken EvaluateFunction(FunctionExpression functionExpression, FunctionArgument[] parameters, IEvaluationContext context, TemplateErrorAdditionalInfo? additionalnfo) { if (functionExpression.Function.StartsWithOrdinalInsensitively(LanguageConstants.ListFunctionPrefix) && this.config.OnListFunc is not null) { var resourceId = parameters[0].TryGetToken()?.Value<string>() ?? throw new UnreachableException(); var apiVersion = parameters[1].TryGetToken()?.Value<string>() ?? throw new UnreachableException(); var body = parameters.Length > 2 ? parameters[2].TryGetToken() : null; return this.config.OnListFunc(functionExpression.Function, resourceId, apiVersion, body); } if (functionExpression.Function.EqualsOrdinalInsensitively("reference")) { var resourceId = parameters[0].TryGetToken()?.Value<string>() ?? throw new UnreachableException(); var apiVersion = parameters.Length > 1 ? (parameters[1].TryGetToken()?.Value<string>() ?? throw new UnreachableException()) : null; var fullBody = parameters.Length > 2 && parameters[2].TryGetToken()?.Value<string>() is { } fullBodyParam && StringComparer.OrdinalIgnoreCase.Equals(fullBodyParam, "Full"); if (apiVersion is not null && this.config.OnReferenceFunc is not null) { return this.config.OnReferenceFunc(resourceId, apiVersion, fullBody); } if (this.resourceLookup.TryGetValue(resourceId, out var foundResource) && (apiVersion is null || StringComparer.OrdinalIgnoreCase.Equals(apiVersion, foundResource.ApiVersion.Value))) { return fullBody ? foundResource.ToJToken() : foundResource.Properties.ToJToken(); } } return this.context.EvaluateFunction(functionExpression, parameters, context, additionalnfo); } public bool ShouldIgnoreExceptionDuringEvaluation(Exception exception) => this.context.ShouldIgnoreExceptionDuringEvaluation(exception); public IEvaluationContext WithNewScope(ExpressionScope scope) => new TemplateEvaluationContext(this.context, scope, this.resourceLookup, this.config); } private static readonly string DummyTenantId = Guid.Empty.ToString(); private static readonly string DummyManagementGroupName = Guid.Empty.ToString(); private static readonly string DummySubscriptionId = Guid.Empty.ToString(); private const string DummyResourceGroupName = "DummyResourceGroup"; private const string DummyLocation = "Dummy Location"; [GeneratedRegex(@"https?://schema\.management\.azure\.com/schemas/[0-9a-zA-Z-]+/(?<templateType>[a-zA-Z]+)Template\.json#?", RegexOptions.IgnoreCase, "en-US")] private static partial Regex templateSchemaPattern(); public delegate JToken OnListDelegate(string functionName, string resourceId, string apiVersion, JToken? body); public delegate JToken OnReferenceDelegate(string resourceId, string apiVersion, bool fullBody); public record EvaluationConfiguration( string TenantId, string ManagementGroup, string SubscriptionId, string ResourceGroup, string RgLocation, Dictionary<string, JToken> Metadata, OnListDelegate? OnListFunc, OnReferenceDelegate? OnReferenceFunc) { public static EvaluationConfiguration Default = new( DummyTenantId, DummyManagementGroupName, DummySubscriptionId, DummyResourceGroupName, DummyLocation, new(), null, null ); } private static string GetResourceId(string scopeString, TemplateResource resource) { var typeSegments = resource.Type.Value.Split('/'); var nameSegments = resource.Name.Value.Split('/'); var types = new[] { typeSegments.First() } .Concat(typeSegments.Skip(1).Zip(nameSegments, (type, name) => $"{type}/{name}")); return $"{scopeString}providers/{string.Join('/', types)}"; } private static void ProcessTemplateLanguageExpressions(Template template, EvaluationConfiguration config, TemplateDeploymentScope deploymentScope) { var scopeString = deploymentScope switch { TemplateDeploymentScope.Tenant => "/", TemplateDeploymentScope.ManagementGroup => $"/providers/Microsoft.Management/managementGroups/{config.ManagementGroup}/", TemplateDeploymentScope.Subscription => $"/subscriptions/{config.SubscriptionId}/", TemplateDeploymentScope.ResourceGroup => $"/subscriptions/{config.SubscriptionId}/resourceGroups/{config.ResourceGroup}/", _ => throw new InvalidOperationException(), }; var resourceLookup = template.Resources.ToOrdinalInsensitiveDictionary(x => GetResourceId(scopeString, x)); var evaluationContext = TemplateEvaluationContext.Create(template, resourceLookup, config); for (int i = 0; i < template.Resources.Length; i++) { var resource = template.Resources[i]; if (resource.Properties is not null) { var skipEvaluationPaths = new InsensitiveHashSet(); if (resource.Type.Value.EqualsOrdinalInsensitively("Microsoft.Resources/deployments")) { skipEvaluationPaths.Add("template"); }; resource.Properties.Value = ExpressionsEngine.EvaluateLanguageExpressionsRecursive( root: resource.Properties.Value, evaluationContext: evaluationContext, skipEvaluationPaths: skipEvaluationPaths); } } if (template.Outputs is not null && template.Outputs.Count > 0) { foreach (var outputKey in template.Outputs.Keys.ToList()) { template.Outputs[outputKey].Value.Value = ExpressionsEngine.EvaluateLanguageExpressionsOptimistically( root: template.Outputs[outputKey].Value.Value, evaluationContext: evaluationContext); } } } public static Template Evaluate(JToken? templateJtoken, JToken? parametersJToken = null, Func<EvaluationConfiguration, EvaluationConfiguration>? configBuilder = null, IFeatureProvider? features = null) { var configuration = EvaluationConfiguration.Default; if (configBuilder is not null) { configuration = configBuilder(configuration); } return EvaluateTemplate(templateJtoken, parametersJToken, configuration, features); } private static Template EvaluateTemplate(JToken? templateJtoken, JToken? parametersJToken, EvaluationConfiguration config, IFeatureProvider? features) { templateJtoken = templateJtoken ?? throw new ArgumentNullException(nameof(templateJtoken)); var deploymentScope = GetDeploymentScope(templateJtoken["$schema"]!.ToString()); var metadata = new InsensitiveDictionary<JToken>(config.Metadata); if (deploymentScope == TemplateDeploymentScope.Subscription || deploymentScope == TemplateDeploymentScope.ResourceGroup) { metadata["subscription"] = new JObject { ["id"] = $"/subscriptions/{config.SubscriptionId}", ["subscriptionId"] = config.SubscriptionId, ["tenantId"] = config.TenantId, }; } if (deploymentScope == TemplateDeploymentScope.ResourceGroup) { metadata["resourceGroup"] = new JObject { ["id"] = $"/subscriptions/{config.SubscriptionId}/resourceGroups/{config.ResourceGroup}", ["location"] = config.RgLocation, }; }; if (deploymentScope == TemplateDeploymentScope.ManagementGroup) { metadata["managementGroup"] = new JObject { ["id"] = $"/providers/Microsoft.Management/managementGroups/{config.ManagementGroup}", ["name"] = config.ManagementGroup, ["type"] = "Microsoft.Management/managementGroups", }; }; // tenant() function is available at all scopes metadata["tenant"] = new JObject { ["tenantId"] = config.TenantId, }; try { var template = TemplateEngine.ParseTemplate(templateJtoken.ToString()); var parameters = ConvertParameters(parametersJToken); var extensionConfigs = ConvertExtensionConfigs(parametersJToken); var expectedApiVersion = features is not null ? EmitConstants.GetNestedDeploymentResourceApiVersion(features) : EmitConstants.NestedDeploymentResourceApiVersion; TemplateEngine.ValidateTemplate(template, expectedApiVersion, deploymentScope); TemplateEngine.ProcessTemplateLanguageExpressions( managementGroupName: config.ManagementGroup, subscriptionId: config.SubscriptionId, resourceGroupName: config.ResourceGroup, template: template, apiVersion: expectedApiVersion, inputParameters: new(parameters), metadata: metadata, extensionConfigs: extensionConfigs, metricsRecorder: new TemplateMetricsRecorder()); ProcessTemplateLanguageExpressions(template, config, deploymentScope); TemplateEngine.ValidateProcessedTemplate(template, expectedApiVersion, deploymentScope); return template; } catch (Exception exception) { throw new InvalidOperationException( $"Evaluating template failed: {exception.Message}." + $"\nTemplate file: {templateJtoken}" + (parametersJToken is null ? "" : $"\nParameters file: {parametersJToken}"), exception); } } private static ImmutableDictionary<string, JToken> ConvertParameters(JToken? parametersJToken) { if (parametersJToken is null) { return ImmutableDictionary<string, JToken>.Empty; } var parametersObject = parametersJToken["parameters"] as JObject; var externalInputsObject = parametersJToken["externalInputs"] as JObject; var externalInputs = externalInputsObject?.Properties().ToImmutableDictionary( x => x.Name, x => new DeploymentExternalInput { Value = x.Value["value"] }) ?? ImmutableDictionary<string, DeploymentExternalInput>.Empty; IntermediateEvaluationContext context = new( [ ExpressionBuiltInFunctions.Functions, new ParametersScope(externalInputs) ], new TemplateMetricsRecorder()); return parametersObject!.Properties().ToImmutableDictionary(x => x.Name, x => { if (x.Value["expression"] is { } expression) { return ToJTokenExpressionSerializer.Serialize(context.EvaluateExpression(ExpressionParser.ParseLanguageExpression(expression))); } return x.Value["value"]!; }); } private static IReadOnlyDictionary<string, IReadOnlyDictionary<string, DeploymentExtensionConfigItem>> ConvertExtensionConfigs(JToken? parametersJToken) => parametersJToken?["extensionConfigs"] is JObject extensionConfigs ? extensionConfigs .FromDeploymentsJToken<OrdinalDictionary<OrdinalDictionary<DeploymentExtensionConfigItem>>>() .ToOrdinalDictionary(kvp => kvp.Key, IReadOnlyDictionary<string, DeploymentExtensionConfigItem> (kvp) => kvp.Value) : []; private static TemplateDeploymentScope GetDeploymentScope(string templateSchema) { var templateSchemaMatch = templateSchemaPattern().Match(templateSchema); var templateType = templateSchemaMatch.Groups["templateType"].Value.ToLowerInvariant(); return templateType switch { "deployment" => TemplateDeploymentScope.ResourceGroup, "subscriptiondeployment" => TemplateDeploymentScope.Subscription, "managementgroupdeployment" => TemplateDeploymentScope.ManagementGroup, "tenantdeployment" => TemplateDeploymentScope.Tenant, _ => throw new InvalidOperationException($"Unrecognized schema: {templateSchema}"), }; } } }