From c7bdcda8164771a696aa579548c1a7e46028019b Mon Sep 17 00:00:00 2001 From: Shenglong Li Date: Thu, 14 Nov 2024 23:37:00 -0800 Subject: [PATCH] Consolidate TemplateEvaluator classes (#15581) Closes #14772. ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/15581) --- src/Bicep.Cli/Services/TemplateEvaluator.cs | 266 ------------------ src/Bicep.Cli/Services/TestRunner.cs | 34 ++- .../CompileTimeImportTests.cs | 7 +- .../Emit/TemplateEmitterTests.cs | 4 +- .../EvaluationTests.cs | 41 +-- .../ExamplesTests.cs | 2 +- .../ParameterFileTests.cs | 2 +- .../ParentPropertyResourceTests.cs | 5 +- .../ScenarioTests.cs | 37 +-- .../SpreadTests.cs | 5 +- .../TemplateHelper.cs | 38 +++ .../UserDefinedFunctionTests.cs | 9 +- .../Utils}/TemplateEvaluator.cs | 69 +++-- 13 files changed, 171 insertions(+), 348 deletions(-) delete mode 100644 src/Bicep.Cli/Services/TemplateEvaluator.cs create mode 100644 src/Bicep.Core.IntegrationTests/TemplateHelper.cs rename src/{Bicep.Core.IntegrationTests => Bicep.Core/Utils}/TemplateEvaluator.cs (83%) diff --git a/src/Bicep.Cli/Services/TemplateEvaluator.cs b/src/Bicep.Cli/Services/TemplateEvaluator.cs deleted file mode 100644 index d0c56083e5a..00000000000 --- a/src/Bicep.Cli/Services/TemplateEvaluator.cs +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -using System.Collections.Immutable; -using System.Diagnostics; -using System.Text.RegularExpressions; -using Azure.Deployments.Core.Configuration; -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.Templates.Engines; -using Bicep.Core; -using Bicep.Core.Emit; -using Microsoft.WindowsAzure.ResourceStack.Common.Collections; -using Microsoft.WindowsAzure.ResourceStack.Common.Extensions; -using Newtonsoft.Json.Linq; - -namespace Bicep.Cli.Services -{ - 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 resourceLookup; - private readonly EvaluationConfiguration config; - - private TemplateEvaluationContext(IEvaluationContext context, ExpressionScope scope, OrdinalInsensitiveDictionary resourceLookup, EvaluationConfiguration config) - { - this.context = context; - this.Scope = scope; - this.resourceLookup = resourceLookup; - this.config = config; - } - - public static TemplateEvaluationContext Create(Template template, OrdinalInsensitiveDictionary 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() ?? throw new UnreachableException(); - var apiVersion = parameters[1].TryGetToken()?.Value() ?? 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() ?? throw new UnreachableException(); - var apiVersion = parameters.Length > 1 ? (parameters[1].TryGetToken()?.Value() ?? throw new UnreachableException()) : null; - var fullBody = parameters.Length > 2 && parameters[2].TryGetToken()?.Value() 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 const string DummyTenantId = ""; - private const string DummyManagementGroupName = ""; - private const string DummySubscriptionId = ""; - private const string DummyResourceGroupName = ""; - private const string DummyLocation = ""; - - [GeneratedRegex(@"https?://schema\.management\.azure\.com/schemas/[0-9a-zA-Z-]+/(?[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 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 TestEvaluation Evaluate(JToken? templateJtoken, JToken? parametersJToken = null, Func? configBuilder = null) - { - var configuration = EvaluationConfiguration.Default; - - if (configBuilder is not null) - { - configuration = configBuilder(configuration); - } - - return EvaluateTemplate(templateJtoken, parametersJToken, configuration); - } - - private static TestEvaluation EvaluateTemplate(JToken? templateJtoken, JToken? parametersJToken, EvaluationConfiguration config) - { - templateJtoken = templateJtoken ?? throw new ArgumentNullException(nameof(templateJtoken)); - - var deploymentScope = GetDeploymentScope(templateJtoken["$schema"]!.ToString()); - - var metadata = new InsensitiveDictionary(); - - try - { - var template = TemplateEngine.ParseTemplate(templateJtoken.ToString()); - var parameters = ParseParametersFile(parametersJToken); - - TemplateEngine.ValidateTemplate(template, EmitConstants.NestedDeploymentResourceApiVersion, deploymentScope); - - TemplateEngine.ProcessTemplateLanguageExpressions( - managementGroupName: config.ManagementGroup, - subscriptionId: config.SubscriptionId, - resourceGroupName: config.ResourceGroup, - template: template, - apiVersion: EmitConstants.NestedDeploymentResourceApiVersion, - inputParameters: new(parameters), - metadata: metadata, - metricsRecorder: new TemplateMetricsRecorder()); - - ProcessTemplateLanguageExpressions(template, config, deploymentScope); - - TemplateEngine.ValidateProcessedTemplate(template, EmitConstants.NestedDeploymentResourceApiVersion, deploymentScope); - - var allAssertions = template.Asserts?.Select(p => new AssertionResult(p.Key, (bool)p.Value.Value)).ToImmutableArray() ?? []; - var failedAssertions = allAssertions.Where(a => !a.Result).Select(a => a).ToImmutableArray(); - return new TestEvaluation(template, null, allAssertions, failedAssertions); - } - catch (Exception exception) - { - var error = exception.Message; - - return new TestEvaluation(null, error, [], []); - } - } - - private static ImmutableDictionary ParseParametersFile(JToken? parametersJToken) - { - if (parametersJToken is null) - { - return ImmutableDictionary.Empty; - } - - return parametersJToken.Cast().ToImmutableDictionary(x => x.Name, x => x.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}"), - }; - } - } -} - diff --git a/src/Bicep.Cli/Services/TestRunner.cs b/src/Bicep.Cli/Services/TestRunner.cs index 19641355ab8..9ab29c2e2dc 100644 --- a/src/Bicep.Cli/Services/TestRunner.cs +++ b/src/Bicep.Cli/Services/TestRunner.cs @@ -2,10 +2,12 @@ // Licensed under the MIT License. using System.Collections.Immutable; +using Azure.Deployments.Core.Definitions.Schema; using Bicep.Core.Emit; using Bicep.Core.Intermediate; using Bicep.Core.Semantics; using Bicep.Core.Syntax; +using Bicep.Core.Utils; using Microsoft.WindowsAzure.ResourceStack.Common.Json; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -37,9 +39,23 @@ public static TestResults Run(ImmutableArray testDeclarations) semanticModel is SemanticModel testSemanticModel) { var parameters = TryGetParameters(testSemanticModel, testDeclaration); - var template = GetTemplate(testSemanticModel); + var templateJToken = GetTemplate(testSemanticModel); + TestEvaluation evaluation; + + try + { + var template = TemplateEvaluator.Evaluate(templateJToken, parameters); + var allAssertions = template.Asserts?.Select(p => new AssertionResult(p.Key, (bool)p.Value.Value)).ToImmutableArray() ?? []; + var failedAssertions = allAssertions.Where(a => !a.Result).Select(a => a).ToImmutableArray(); + + evaluation = new TestEvaluation(template, null, allAssertions, failedAssertions); + } + catch (Exception exception) + { + var error = exception.Message; + evaluation = new TestEvaluation(null, error, [], []); + } - var evaluation = TemplateEvaluator.Evaluate(template, parameters); var testResult = new TestResult(testDeclaration, evaluation); testResults.Add(testResult); @@ -84,7 +100,19 @@ private static JToken GetTemplate(SemanticModel model) new TemplateWriter(model).EmitTestParameters(emitter, parametersExpression); writer.Flush(); - return textWriter.ToString().FromJson(); + var parameters = textWriter.ToString().FromJson().Properties() + .ToDictionary(x => x.Name, x => new JObject() + { + ["value"] = x.Value, + }) + .ToJToken(); + + return new JObject() + { + ["$schema"] = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + ["contentVersion"] = "1.0.0.0", + ["parameters"] = parameters, + }; } return null; diff --git a/src/Bicep.Core.IntegrationTests/CompileTimeImportTests.cs b/src/Bicep.Core.IntegrationTests/CompileTimeImportTests.cs index be1d5d0c3fa..c9c48f4e0da 100644 --- a/src/Bicep.Core.IntegrationTests/CompileTimeImportTests.cs +++ b/src/Bicep.Core.IntegrationTests/CompileTimeImportTests.cs @@ -6,6 +6,7 @@ using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Utils; +using Bicep.Core.Utils; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; @@ -1620,7 +1621,7 @@ param intParam int result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics(); - var parameters = TemplateEvaluator.ParseParametersFile(result.Parameters); + var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters); parameters["intParam"].Should().DeepEqual(9); } @@ -1649,7 +1650,7 @@ param intParam int result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics(); - var parameters = TemplateEvaluator.ParseParametersFile(result.Parameters); + var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters); parameters["intParam"].Should().DeepEqual(9); } @@ -1925,7 +1926,7 @@ func isWindows(hostingPlanName string) string => contains('-windows-', hostingPl result.Diagnostics.Should().BeEmpty(); result.Template.Should().NotBeNull(); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("outputs.out.value", "ASP999"); } diff --git a/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs b/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs index df13dc2be8d..47ca58bfa1d 100644 --- a/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs +++ b/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs @@ -75,7 +75,7 @@ public async Task ValidBicep_TemplateEmiterShouldProduceExpectedTemplate(DataSet actualLocation: compiledFilePath); // validate that the template is parseable by the deployment engine - TemplateHelper.TemplateShouldBeValid(outputFile); + UnitTests.Utils.TemplateHelper.TemplateShouldBeValid(outputFile); } [DataTestMethod] @@ -100,7 +100,7 @@ public async Task ValidBicep_EmitTemplate_should_produce_expected_symbolicname_t actualLocation: compiledFilePath); // validate that the template is parseable by the deployment engine - TemplateHelper.TemplateShouldBeValid(outputFile); + UnitTests.Utils.TemplateHelper.TemplateShouldBeValid(outputFile); } [DataTestMethod] diff --git a/src/Bicep.Core.IntegrationTests/EvaluationTests.cs b/src/Bicep.Core.IntegrationTests/EvaluationTests.cs index a53df6d74f7..08b33e87105 100644 --- a/src/Bicep.Core.IntegrationTests/EvaluationTests.cs +++ b/src/Bicep.Core.IntegrationTests/EvaluationTests.cs @@ -6,6 +6,7 @@ using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Utils; +using Bicep.Core.Utils; using FluentAssertions; using FluentAssertions.Execution; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -38,7 +39,7 @@ public void Basic_arithmetic_expressions_are_evaluated_successfully() using (new AssertionScope()) { - var evaluated = TemplateEvaluator.Evaluate(template); + var evaluated = TemplateEvaluator.Evaluate(template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['sum'].value", 4); evaluated.Should().HaveValueAtPath("$.outputs['mult'].value", 20); @@ -79,7 +80,7 @@ are not using (new AssertionScope()) { - var evaluated = TemplateEvaluator.Evaluate(template); + var evaluated = TemplateEvaluator.Evaluate(template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['literal'].value", "hello!"); evaluated.Should().HaveValueAtPath("$.outputs['interp'].value", ">False<>12948<>hello!<>{'a':'b','!c':2}<>[true,2893,'abc']<"); @@ -167,7 +168,7 @@ param childName string { SubscriptionId = testSubscriptionId, ResourceGroup = testRgName, - }); + }).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['resource1Id'].value", $"/subscriptions/{testSubscriptionId}/resourceGroups/{testRgName}/providers/My.Rp/parent/myParent/child/myChild"); evaluated.Should().HaveValueAtPath("$.outputs['resource2Id'].value", $"/subscriptions/{testSubscriptionId}/resourceGroups/customRg/providers/My.Rp/parent/myParent/child/myChild"); @@ -201,7 +202,7 @@ public void Comparisons_are_evaluated_correctly() using (new AssertionScope()) { - var evaluated = TemplateEvaluator.Evaluate(template); + var evaluated = TemplateEvaluator.Evaluate(template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['less'].value", true); evaluated.Should().HaveValueAtPath("$.outputs['lessOrEquals'].value", true); @@ -243,7 +244,7 @@ param abcVal string using (new AssertionScope()) { - var evaluated = TemplateEvaluator.Evaluate(template, parameters); + var evaluated = TemplateEvaluator.Evaluate(template, parameters).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['abcVal'].value", "test!!!"); } @@ -276,7 +277,7 @@ public void Existing_resource_property_access_works() throw new NotImplementedException(); }, - }); + }).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['abcVal'].value", "test!!!"); } @@ -311,7 +312,7 @@ param inputObj object var result = CompilationHelper.Compile(bicepTemplateText); - var evaluated = TemplateEvaluator.Evaluate(result.Template, parameters); + var evaluated = TemplateEvaluator.Evaluate(result.Template, parameters).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['inputObjKeys'].value", new JArray { @@ -359,7 +360,7 @@ public void Join_function_evaluation_works() result.Should().NotHaveAnyDiagnostics(); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['joined1'].value", "abcdefghi"); evaluated.Should().HaveValueAtPath("$.outputs['joined2'].value", "abc,def,ghi"); evaluated.Should().HaveValueAtPath("$.outputs['joined3'].value", "I love Bicep"); @@ -412,7 +413,7 @@ param inputArray array using (new AssertionScope()) { - var evaluated = TemplateEvaluator.Evaluate(template, parameters); + var evaluated = TemplateEvaluator.Evaluate(template, parameters).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['strIdxFooLC'].value", new JValue(0)); evaluated.Should().HaveValueAtPath("$.outputs['strIdxFooUC'].value", new JValue(0)); @@ -520,7 +521,7 @@ param numbers array using (new AssertionScope()) { - var evaluated = TemplateEvaluator.Evaluate(template, parameters); + var evaluated = TemplateEvaluator.Evaluate(template, parameters).ToJToken(); evaluated.Should().HaveValueAtPath("$.variables['sayHello']", new JArray { @@ -735,7 +736,7 @@ public void Issue8782() }] "); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['testMap'].value", JToken.Parse(@" [ { @@ -794,7 +795,7 @@ param testObject object var result = CompilationHelper.Compile(bicepTemplateText); - var evaluated = TemplateEvaluator.Evaluate(result.Template, parameters); + var evaluated = TemplateEvaluator.Evaluate(result.Template, parameters).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['output1'].value", JToken.Parse(@"[ [ ""no"" @@ -846,7 +847,7 @@ public void Issue8798() output iDogs array = filter(dogs, dog => (contains(dog.name, 'C') || contains(dog.name, 'i'))) "); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['iDogs'].value", JToken.Parse(@" [ { @@ -943,7 +944,7 @@ param bar string } }"); }, - }); + }).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['test1'].value", "abc"); evaluated.Should().HaveValueAtPath("$.outputs['test2'].value", "def"); @@ -974,7 +975,7 @@ param param2 object using (new AssertionScope()) { - var evaluated = TemplateEvaluator.Evaluate(template, parameters); + var evaluated = TemplateEvaluator.Evaluate(template, parameters).ToJToken(); diagnostics.Should().NotHaveAnyDiagnostics(); @@ -1064,7 +1065,7 @@ public void Safe_dereferences_are_evaluated_successfully() using (new AssertionScope()) { - var evaluated = TemplateEvaluator.Evaluate(template); + var evaluated = TemplateEvaluator.Evaluate(template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['properties'].value.exists", "baz"); evaluated.Should().HaveValueAtPath("$.outputs['properties'].value.doesntExist", JValue.CreateNull()); @@ -1113,7 +1114,7 @@ param location string using (new AssertionScope()) { - var evaluated = TemplateEvaluator.Evaluate(template, parameters); + var evaluated = TemplateEvaluator.Evaluate(template, parameters).ToJToken(); evaluated.Should().HaveValueAtPath("$.asserts['a1']", false); evaluated.Should().HaveValueAtPath("$.asserts['a2']", true); @@ -1130,7 +1131,7 @@ param foo string? output foo string = foo ?? 'not specified' """); - var evaluated = TemplateEvaluator.Evaluate(template); + var evaluated = TemplateEvaluator.Evaluate(template).ToJToken(); evaluated.Should().HaveValueAtPath("$.languageVersion", "2.0"); evaluated.Should().HaveValueAtPath("$.outputs.foo.value", "not specified"); @@ -1160,7 +1161,7 @@ func replaceMultiple(value string, input object) string => reduce( using (new AssertionScope()) { - var evaluated = TemplateEvaluator.Evaluate(template); + var evaluated = TemplateEvaluator.Evaluate(template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['sayHiWithComposition'].value", "Hi, Anthony Martin!"); evaluated.Should().HaveValueAtPath("$.outputs['sayHiWithLambdas'].value", "Hi, Anthony Martin!"); @@ -1214,7 +1215,7 @@ func isEven(i int) bool => i % 2 == 0 using (new AssertionScope()) { - var evaluated = TemplateEvaluator.Evaluate(template); + var evaluated = TemplateEvaluator.Evaluate(template).ToJToken(); evaluated.Should().HaveJsonAtPath("$.outputs['sayHello'].value", """ [ diff --git a/src/Bicep.Core.IntegrationTests/ExamplesTests.cs b/src/Bicep.Core.IntegrationTests/ExamplesTests.cs index aefbaf30407..8c84e379642 100644 --- a/src/Bicep.Core.IntegrationTests/ExamplesTests.cs +++ b/src/Bicep.Core.IntegrationTests/ExamplesTests.cs @@ -60,7 +60,7 @@ private async Task RunExampleTest(EmbeddedFile embeddedBicep, FeatureProviderOve jsonFile.ShouldHaveExpectedJsonValue(); // validate that the template is parseable by the deployment engine - TemplateHelper.TemplateShouldBeValid(stringWriter.ToString()); + UnitTests.Utils.TemplateHelper.TemplateShouldBeValid(stringWriter.ToString()); } } } diff --git a/src/Bicep.Core.IntegrationTests/ParameterFileTests.cs b/src/Bicep.Core.IntegrationTests/ParameterFileTests.cs index 21a35edb383..bc2d6e61c6c 100644 --- a/src/Bicep.Core.IntegrationTests/ParameterFileTests.cs +++ b/src/Bicep.Core.IntegrationTests/ParameterFileTests.cs @@ -148,7 +148,7 @@ param baz string ")); result.Diagnostics.Should().NotHaveAnyDiagnostics(); - var parameters = TemplateEvaluator.ParseParametersFile(result.Parameters); + var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters); parameters["foo"].Should().DeepEqual("abc"); parameters["bar"].Should().DeepEqual(new JObject diff --git a/src/Bicep.Core.IntegrationTests/ParentPropertyResourceTests.cs b/src/Bicep.Core.IntegrationTests/ParentPropertyResourceTests.cs index c46bff22641..4fc110a34b3 100644 --- a/src/Bicep.Core.IntegrationTests/ParentPropertyResourceTests.cs +++ b/src/Bicep.Core.IntegrationTests/ParentPropertyResourceTests.cs @@ -3,6 +3,7 @@ using Bicep.Core.Diagnostics; using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Utils; +using Bicep.Core.Utils; using FluentAssertions; using FluentAssertions.Execution; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -151,14 +152,14 @@ public void Parent_property_works_with_extension_resource_collections() result.Diagnostics.ExcludingLinterDiagnostics().ExcludingMissingTypes().Should().BeEmpty(); var compiled = result.Template; - var evaluated = TemplateEvaluator.Evaluate(compiled); + var evaluated = TemplateEvaluator.Evaluate(compiled).ToJToken(); compiled.Should().HaveValueAtPath("$.outputs['otherRes2childName'].value", "[format('otherResChild{0}', sub(range(30, 10)[2], 30))]"); evaluated.Should().HaveValueAtPath("$.outputs['otherRes2childName'].value", "otherResChild2"); compiled.Should().HaveValueAtPath("$.outputs['otherRes2childType'].value", "Microsoft.Rp2/resource2/child2"); evaluated.Should().HaveValueAtPath("$.outputs['otherRes2childType'].value", "Microsoft.Rp2/resource2/child2"); compiled.Should().HaveValueAtPath("$.outputs['otherRes2childId'].value", "[extensionResourceId(resourceId('Microsoft.Rp1/resource1/child1', format('res{0}', range(0, 10)[sub(range(10, 10)[sub(range(30, 10)[2], 30)], 10)]), format('child{0}', sub(range(10, 10)[sub(range(30, 10)[2], 30)], 10))), 'Microsoft.Rp2/resource2/child2', format('otherRes{0}', sub(range(20, 10)[sub(range(30, 10)[2], 30)], 20)), format('otherResChild{0}', sub(range(30, 10)[2], 30)))]"); - evaluated.Should().HaveValueAtPath("$.outputs['otherRes2childId'].value", "/subscriptions/f91a30fd-f403-4999-ae9f-ec37a6d81e13/resourceGroups/testResourceGroup/providers/Microsoft.Rp1/resource1/res2/child1/child2/providers/Microsoft.Rp2/resource2/otherRes2/child2/otherResChild2"); + evaluated.Should().HaveValueAtPath("$.outputs['otherRes2childId'].value", $"/subscriptions/{Guid.Empty}/resourceGroups/DummyResourceGroup/providers/Microsoft.Rp1/resource1/res2/child1/child2/providers/Microsoft.Rp2/resource2/otherRes2/child2/otherResChild2"); } [TestMethod] diff --git a/src/Bicep.Core.IntegrationTests/ScenarioTests.cs b/src/Bicep.Core.IntegrationTests/ScenarioTests.cs index d2a9db971e0..ee8ad58b597 100644 --- a/src/Bicep.Core.IntegrationTests/ScenarioTests.cs +++ b/src/Bicep.Core.IntegrationTests/ScenarioTests.cs @@ -13,6 +13,7 @@ using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Features; using Bicep.Core.UnitTests.Utils; +using Bicep.Core.Utils; using FluentAssertions; using FluentAssertions.Execution; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -1294,7 +1295,7 @@ public void Test_Issue1993() "); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); var expectedOutput = new JArray { new JObject {["element"] = "one"}, @@ -1346,7 +1347,7 @@ public void Test_Issue2009() { ["providers"] = JToken.FromObject(providersMetadata), } - }); + }).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['providerOutput'].value.thing", new JObject { @@ -1413,7 +1414,7 @@ public void Test_Issue2009_expanded() { ["providers"] = JToken.FromObject(providersMetadata), } - }); + }).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['providersNamespace'].value", "Test.Rp"); evaluated.Should().HaveValueAtPath("$.outputs['providersResources'].value", new JArray @@ -1520,8 +1521,8 @@ public void Test_Issue2535() result.ExcludingLinterDiagnostics().Should().NotHaveAnyCompilationBlockingDiagnostics(); result.Template.Should().HaveValueAtPath("$.resources[0].properties.details.parent", "[managementGroup()]"); - var evaluated = TemplateEvaluator.Evaluate(result.Template); - evaluated.Should().HaveValueAtPath("$.resources[0].properties.details.parent.id", "/providers/Microsoft.Management/managementGroups/3fc9f36e-8699-43af-b038-1c103980942f"); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); + evaluated.Should().HaveValueAtPath("$.resources[0].properties.details.parent.id", $"/providers/Microsoft.Management/managementGroups/{Guid.Empty}"); } [TestMethod] @@ -1595,10 +1596,10 @@ public void Test_Issue2268() "); result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics(); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.resources[0].name", "myServer/myDb/current"); - evaluated.Should().HaveValueAtPath("$.outputs['tdeId'].value", "/subscriptions/f91a30fd-f403-4999-ae9f-ec37a6d81e13/resourceGroups/testResourceGroup/providers/Microsoft.Sql/servers/myServer/databases/myDb/transparentDataEncryption/current"); + evaluated.Should().HaveValueAtPath("$.outputs['tdeId'].value", $"/subscriptions/{Guid.Empty}/resourceGroups/DummyResourceGroup/providers/Microsoft.Sql/servers/myServer/databases/myDb/transparentDataEncryption/current"); } [TestMethod] @@ -2357,7 +2358,7 @@ public void Test_Issue4007() result.Template.Should().HaveValueAtPath("$.outputs.one.value", "[variables('map')['1']]"); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs.one.value", "hello"); } @@ -2567,7 +2568,7 @@ public void Test_Issue4565() result.Template.Should().HaveValueAtPath("$.outputs['test'].value", "[format('{0}', variables('port'))]"); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['test'].value", "1234", "the evaluated output should be of type string"); } @@ -2612,8 +2613,8 @@ public void Test_Issue1228() result.Template.Should().HaveValueAtPath("$.resources[?(@.name == 'Default initiative')].properties.policyDefinitions[0].policyDefinitionId", "[extensionResourceId(managementGroup().id, 'Microsoft.Authorization/policyDefinitions', 'Allowed locations')]"); - var evaluated = TemplateEvaluator.Evaluate(result.Template); - evaluated.Should().HaveValueAtPath("$.resources[?(@.name == 'Default initiative')].properties.policyDefinitions[0].policyDefinitionId", "/providers/Microsoft.Management/managementGroups/3fc9f36e-8699-43af-b038-1c103980942f/providers/Microsoft.Authorization/policyDefinitions/Allowed locations"); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); + evaluated.Should().HaveValueAtPath("$.resources[?(@.name == 'Default initiative')].properties.policyDefinitions[0].policyDefinitionId", $"/providers/Microsoft.Management/managementGroups/{Guid.Empty}/providers/Microsoft.Authorization/policyDefinitions/Allowed locations"); } // https://github.com/Azure/bicep/issues/4850 @@ -4166,8 +4167,8 @@ public void Test_Issue9246() output aksRouteTable string = _subnets.aksPoolSys.routeTable ")); - var evaluated = TemplateEvaluator.Evaluate(result.Template); - evaluated.Should().HaveValueAtPath("$.outputs['aksRouteTable'].value", "/subscriptions/f91a30fd-f403-4999-ae9f-ec37a6d81e13/resourceGroups/testResourceGroup/providers/Microsoft.Network/routeTables/aksRouteTable"); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); + evaluated.Should().HaveValueAtPath("$.outputs['aksRouteTable'].value", $"/subscriptions/{Guid.Empty}/resourceGroups/DummyResourceGroup/providers/Microsoft.Network/routeTables/aksRouteTable"); } // https://github.com/Azure/bicep/issues/9285 @@ -4438,7 +4439,7 @@ param CertificateSubjects certificateMapping[] result.Should().GenerateATemplate(); - var evaluated = TemplateEvaluator.Evaluate(result.Template, parameters); + var evaluated = TemplateEvaluator.Evaluate(result.Template, parameters).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['vaultId'].value", "/subscriptions/mySub/resourceGroups/myRg/providers/Microsoft.KeyVault/vaults/myKv"); } @@ -5064,7 +5065,7 @@ public void Test_Issue10343() } ")); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("resources.foo3.dependsOn", new JArray("foo2")); } @@ -5490,7 +5491,7 @@ func test4() string => loadFileAsBase64('./repro-data.json') """)); result.Should().NotHaveAnyDiagnostics(); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.functions[0].members['test'].output.value", new JObject()); evaluated.Should().HaveValueAtPath("$.functions[0].members['test2'].output.value", "{}"); evaluated.Should().HaveValueAtPath("$.functions[0].members['test3'].output.value", new JObject()); @@ -5521,7 +5522,7 @@ func MyFunction(name string) string => '${loadJsonContent('./test-mapping.json') """)); result.Should().NotHaveAnyDiagnostics(); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['foo'].value", "bar"); } @@ -6048,7 +6049,7 @@ func genModuleTags(moduleName string) moduleTags => { """), ("compiled.json", moduleResult.Template!.ToString())); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['moduleTags'].value", new JObject { diff --git a/src/Bicep.Core.IntegrationTests/SpreadTests.cs b/src/Bicep.Core.IntegrationTests/SpreadTests.cs index ffdf76bb963..ae0401b5f21 100644 --- a/src/Bicep.Core.IntegrationTests/SpreadTests.cs +++ b/src/Bicep.Core.IntegrationTests/SpreadTests.cs @@ -3,6 +3,7 @@ using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Utils; +using Bicep.Core.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Bicep.Core.IntegrationTests; @@ -29,7 +30,7 @@ public void Spread_operator_results_in_correct_codegen() result.Template.Should().HaveValueAtPath("$.variables['other']['bar']", "[flatten(createArray(createArray(1), createArray(2, 3), createArray(4)))]"); result.Template.Should().HaveValueAtPath("$.outputs['test'].value", "[shallowMerge(createArray(createObject('foo', 'foo'), variables('other'), createObject('baz', 'baz')))]"); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveJsonAtPath("$.outputs['test'].value", """ { "foo": "foo", @@ -210,7 +211,7 @@ public void Object_spread_edge_cases() result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics(); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveJsonAtPath("$.outputs['test1'].value", """ { "a": 1, diff --git a/src/Bicep.Core.IntegrationTests/TemplateHelper.cs b/src/Bicep.Core.IntegrationTests/TemplateHelper.cs new file mode 100644 index 00000000000..a96ae48fa93 --- /dev/null +++ b/src/Bicep.Core.IntegrationTests/TemplateHelper.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.Collections.Immutable; +using System.Diagnostics; +using Azure.Deployments.Core.Configuration; +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.Templates.Engines; +using Bicep.Core.Emit; +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Utils; +using Microsoft.WindowsAzure.ResourceStack.Common.Collections; +using Microsoft.WindowsAzure.ResourceStack.Common.Extensions; +using Newtonsoft.Json.Linq; + +namespace Bicep.Core.IntegrationTests +{ + public class TemplateHelper + { + public static ImmutableDictionary ConvertAndAssertParameters(JToken? parametersJToken) + { + if (parametersJToken is null) + { + return ImmutableDictionary.Empty; + } + + parametersJToken.Should().HaveValueAtPath("$schema", "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#"); + parametersJToken.Should().HaveValueAtPath("contentVersion", "1.0.0.0"); + var parametersObject = parametersJToken["parameters"] as JObject; + parametersObject.Should().NotBeNull(); + + return parametersObject!.Properties().ToImmutableDictionary(x => x.Name, x => x.Value["value"]!); + } + } +} diff --git a/src/Bicep.Core.IntegrationTests/UserDefinedFunctionTests.cs b/src/Bicep.Core.IntegrationTests/UserDefinedFunctionTests.cs index ff704a6b6b0..c8e3037d644 100644 --- a/src/Bicep.Core.IntegrationTests/UserDefinedFunctionTests.cs +++ b/src/Bicep.Core.IntegrationTests/UserDefinedFunctionTests.cs @@ -4,6 +4,7 @@ using Bicep.Core.Diagnostics; using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Utils; +using Bicep.Core.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Bicep.Core.IntegrationTests; @@ -23,7 +24,7 @@ func buildUrl(https bool, hostname string, path string) string => '${https ? 'ht "); result.Should().NotHaveAnyDiagnostics(); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['foo'].value", "https://google.com/search"); } @@ -59,7 +60,7 @@ func greet(name string) string => format(greeting, name) ")]); result.Should().NotHaveAnyDiagnostics(); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['outputFoo'].value", "Hello userName!"); } @@ -80,7 +81,7 @@ func isStringEqual(input string) bool => input == testFunc(bar) "); result.Should().NotHaveAnyDiagnostics(); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['outputBool'].value", true); evaluated.Should().HaveValueAtPath("$.outputs['outputFoo'].value", "abc-def-baz"); @@ -118,7 +119,7 @@ func returnFoo() string => 'foo' "); result.Should().NotHaveAnyDiagnostics(); - var evaluated = TemplateEvaluator.Evaluate(result.Template); + var evaluated = TemplateEvaluator.Evaluate(result.Template).ToJToken(); evaluated.Should().HaveValueAtPath("$.outputs['outputFoo'].value", "foo"); } diff --git a/src/Bicep.Core.IntegrationTests/TemplateEvaluator.cs b/src/Bicep.Core/Utils/TemplateEvaluator.cs similarity index 83% rename from src/Bicep.Core.IntegrationTests/TemplateEvaluator.cs rename to src/Bicep.Core/Utils/TemplateEvaluator.cs index 752c9e1e5ca..ac142fb47a0 100644 --- a/src/Bicep.Core.IntegrationTests/TemplateEvaluator.cs +++ b/src/Bicep.Core/Utils/TemplateEvaluator.cs @@ -1,7 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. + +using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; using Azure.Deployments.Core.Configuration; using Azure.Deployments.Core.Definitions.Schema; using Azure.Deployments.Core.Diagnostics; @@ -10,15 +17,13 @@ using Azure.Deployments.Expression.Expressions; using Azure.Deployments.Templates.Engines; using Bicep.Core.Emit; -using Bicep.Core.UnitTests.Assertions; -using Bicep.Core.UnitTests.Utils; using Microsoft.WindowsAzure.ResourceStack.Common.Collections; using Microsoft.WindowsAzure.ResourceStack.Common.Extensions; using Newtonsoft.Json.Linq; -namespace Bicep.Core.IntegrationTests +namespace Bicep.Core.Utils { - public class TemplateEvaluator + public partial class TemplateEvaluator { private class NoOpTemplateMetricRecorder : ITemplateMetricsRecorder { @@ -95,12 +100,14 @@ public bool ShouldIgnoreExceptionDuringEvaluation(Exception exception) => public IEvaluationContext WithNewScope(ExpressionScope scope) => new TemplateEvaluationContext(this.context, scope, this.resourceLookup, this.config); } - const string TestTenantId = "d4c73686-f7cd-458e-b377-67adcd46b624"; - const string TestManagementGroupName = "3fc9f36e-8699-43af-b038-1c103980942f"; - const string TestSubscriptionId = "f91a30fd-f403-4999-ae9f-ec37a6d81e13"; - const string TestResourceGroupName = "testResourceGroup"; - const string TestLocation = "West US"; + 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-]+/(?[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); @@ -116,11 +123,11 @@ public record EvaluationConfiguration( OnReferenceDelegate? OnReferenceFunc) { public static EvaluationConfiguration Default = new( - TestTenantId, - TestManagementGroupName, - TestSubscriptionId, - TestResourceGroupName, - TestLocation, + DummyTenantId, + DummyManagementGroupName, + DummySubscriptionId, + DummyResourceGroupName, + DummyLocation, new(), null, null @@ -150,7 +157,6 @@ private static void ProcessTemplateLanguageExpressions(Template template, Evalua }; 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++) @@ -176,14 +182,14 @@ private static void ProcessTemplateLanguageExpressions(Template template, Evalua { foreach (var outputKey in template.Outputs.Keys.ToList()) { - template.Outputs[outputKey].Value.Value = ExpressionsEngine.EvaluateLanguageExpressionsRecursive( + template.Outputs[outputKey].Value.Value = ExpressionsEngine.EvaluateLanguageExpressionsOptimistically( root: template.Outputs[outputKey].Value.Value, evaluationContext: evaluationContext); } } } - public static JToken Evaluate(JToken? templateJtoken, JToken? parametersJToken = null, Func? configBuilder = null) + public static Template Evaluate(JToken? templateJtoken, JToken? parametersJToken = null, Func? configBuilder = null) { var configuration = EvaluationConfiguration.Default; @@ -195,11 +201,11 @@ public static JToken Evaluate(JToken? templateJtoken, JToken? parametersJToken = return EvaluateTemplate(templateJtoken, parametersJToken, configuration); } - private static JToken EvaluateTemplate(JToken? templateJtoken, JToken? parametersJToken, EvaluationConfiguration config) + private static Template EvaluateTemplate(JToken? templateJtoken, JToken? parametersJToken, EvaluationConfiguration config) { templateJtoken = templateJtoken ?? throw new ArgumentNullException(nameof(templateJtoken)); - var deploymentScope = TemplateHelper.GetDeploymentScope(templateJtoken["$schema"]!.ToString()); + var deploymentScope = GetDeploymentScope(templateJtoken["$schema"]!.ToString()); var metadata = new InsensitiveDictionary(config.Metadata); if (deploymentScope == TemplateDeploymentScope.Subscription || deploymentScope == TemplateDeploymentScope.ResourceGroup) @@ -237,7 +243,7 @@ private static JToken EvaluateTemplate(JToken? templateJtoken, JToken? parameter try { var template = TemplateEngine.ParseTemplate(templateJtoken.ToString()); - var parameters = ParseParametersFile(parametersJToken); + var parameters = ConvertParameters(parametersJToken); TemplateEngine.ValidateTemplate(template, EmitConstants.NestedDeploymentResourceApiVersion, deploymentScope); @@ -255,7 +261,7 @@ private static JToken EvaluateTemplate(JToken? templateJtoken, JToken? parameter TemplateEngine.ValidateProcessedTemplate(template, EmitConstants.NestedDeploymentResourceApiVersion, deploymentScope); - return template.ToJToken(); + return template; } catch (Exception exception) { @@ -267,19 +273,30 @@ private static JToken EvaluateTemplate(JToken? templateJtoken, JToken? parameter } } - public static ImmutableDictionary ParseParametersFile(JToken? parametersJToken) + private static ImmutableDictionary ConvertParameters(JToken? parametersJToken) { if (parametersJToken is null) { return ImmutableDictionary.Empty; } - parametersJToken.Should().HaveValueAtPath("$schema", "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#"); - parametersJToken.Should().HaveValueAtPath("contentVersion", "1.0.0.0"); var parametersObject = parametersJToken["parameters"] as JObject; - parametersObject.Should().NotBeNull(); - return parametersObject!.Properties().ToImmutableDictionary(x => x.Name, x => x.Value["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}"), + }; + } } }