diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index 2bfce58112b..48d95ab4c8d 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -29,7 +29,7 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased -What's changed since v1.39.1: +What's changed since v1.39.2: - Engineering: - Migrated Azure samples into PSRule for Azure by @BernieWhite. @@ -37,6 +37,16 @@ What's changed since v1.39.1: - Quality updates to rule documentation by @BernieWhite. [#3102](https://github.com/Azure/PSRule.Rules.Azure/issues/3102) +## v1.39.2 + +What's changed since v1.39.1: + +- Bug fixes: + - Fixed user-defined function reference to exported variable by @BernieWhite. + [#3120](https://github.com/Azure/PSRule.Rules.Azure/issues/3120) + - Fixed name expand of existing resource references by @BernieWhite. + [#3123](https://github.com/Azure/PSRule.Rules.Azure/issues/3123) + ## v1.39.1 What's changed since v1.39.0: diff --git a/src/PSRule.Rules.Azure/Data/Template/ExistingResourceValue.cs b/src/PSRule.Rules.Azure/Data/Template/ExistingResourceValue.cs index a5a543fbc1e..dc82fc88a7f 100644 --- a/src/PSRule.Rules.Azure/Data/Template/ExistingResourceValue.cs +++ b/src/PSRule.Rules.Azure/Data/Template/ExistingResourceValue.cs @@ -70,8 +70,7 @@ public string Name /// private string GetId() { - if (!Value.TryResourceScope(_Context, out var scopeId)) - throw new TemplateSymbolException(SymbolicName); + if (!Value.TryResourceScope(_Context, out var scopeId)) throw new TemplateSymbolException(SymbolicName); return _Id = ResourceHelper.ResourceId(Type, Name, scopeId); } @@ -81,10 +80,9 @@ private string GetId() /// private string GetName() { - if (!Value.TryResourceName(out var name) || string.IsNullOrEmpty(name)) - throw new TemplateSymbolException(SymbolicName); + if (!Value.TryResourceName(out var name) || string.IsNullOrEmpty(name)) throw new TemplateSymbolException(SymbolicName); - return _Name = name.IsExpressionString() ? name.ToString() : name; + return _Name = ExpandString(_Context, name); } } diff --git a/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs b/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs index 1a23332d928..4437093ccfb 100644 --- a/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs @@ -769,37 +769,6 @@ internal void TrackDependencies(IResourceValue resource, string[] dependencies) } } - internal sealed class UserDefinedFunctionContext : NestedTemplateContext - { - private readonly Dictionary _Parameters; - - public UserDefinedFunctionContext(ITemplateContext context) - : base(context) - { - _Parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - public override bool TryParameter(string parameterName, out object value) - { - return _Parameters.TryGetValue(parameterName, out value); - } - - public override bool TryVariable(string variableName, out object value) - { - value = null; - return false; - } - - internal void SetParameters(JObject[] parameters, object[] args) - { - if (parameters == null || parameters.Length == 0 || args == null || args.Length == 0) - return; - - for (var i = 0; i < parameters.Length; i++) - _Parameters.Add(parameters[i]["name"].Value(), args[i]); - } - } - internal interface IParameterValue { string Name { get; } @@ -1169,7 +1138,6 @@ protected virtual void Function(TemplateContext context, string ns, string name, return; TryArrayProperty(function, PROPERTY_PARAMETERS, out var parameters); - //var outputFn = context.Expression.Build(outputValue); ExpressionFn fn = (ctx, args) => { var fnContext = new UserDefinedFunctionContext(ctx); diff --git a/src/PSRule.Rules.Azure/Data/Template/UserDefinedFunctionContext.cs b/src/PSRule.Rules.Azure/Data/Template/UserDefinedFunctionContext.cs new file mode 100644 index 00000000000..f74386db43c --- /dev/null +++ b/src/PSRule.Rules.Azure/Data/Template/UserDefinedFunctionContext.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace PSRule.Rules.Azure.Data.Template; + +#nullable enable + +/// +/// A nested context for user-defined functions. +/// +/// The parent context. +internal sealed class UserDefinedFunctionContext(ITemplateContext context) : NestedTemplateContext(context) +{ + private const string PROPERTY_NAME = "name"; + + private readonly Dictionary _Parameters = new(StringComparer.OrdinalIgnoreCase); + + public override bool TryParameter(string parameterName, out object? value) + { + return _Parameters.TryGetValue(parameterName, out value); + } + + public override bool TryVariable(string variableName, out object? value) + { + return base.TryVariable(variableName, out value); + } + + internal void SetParameters(JObject[] parameters, object[]? args) + { + if (parameters == null || parameters.Length == 0 || args == null || args.Length == 0) + return; + + for (var i = 0; i < parameters.Length; i++) + { + var name = parameters[i][PROPERTY_NAME]?.Value(); + if (!string.IsNullOrEmpty(name)) + { + _Parameters.Add(name!, args[i]); + } + } + } +} + +#nullable restore diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.1.bicep b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.1.bicep index 38cebc4c143..e699262fc2a 100644 --- a/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.1.bicep +++ b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.1.bicep @@ -2,6 +2,7 @@ // Licensed under the MIT License. // Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/2922 +// Based on work contributed by @GABRIELNGBTUC module child_loop './Tests.Bicep.1.child.bicep' = [ for (item, index) in range(0, 2): { diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.bicep b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.bicep new file mode 100644 index 00000000000..ca1ee35451e --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.bicep @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3123 +// Contributed by @CharlesToniolo + +var webAppsNames = [ + 'example1' + 'example2' +] + +resource webApps 'Microsoft.Web/sites@2022-09-01' existing = [ + for webAppName in webAppsNames: { + name: webAppName + } +] + +#disable-next-line no-unused-existing-resources +resource webAppSettings 'Microsoft.Web/sites/config@2022-09-01' existing = [ + for i in range(0, length(webAppsNames)): { + name: 'appsettings' + parent: webApps[i] + } +] diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.json b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.json new file mode 100644 index 00000000000..eea0827c710 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "3757483382650491065" + } + }, + "variables": { + "webAppsNames": [ + "example1", + "example2" + ] + }, + "resources": { + "webApps": { + "copy": { + "name": "webApps", + "count": "[length(variables('webAppsNames'))]" + }, + "existing": true, + "type": "Microsoft.Web/sites", + "apiVersion": "2022-09-01", + "name": "[variables('webAppsNames')[copyIndex()]]" + }, + "webAppSettings": { + "copy": { + "name": "webAppSettings", + "count": "[length(range(0, length(variables('webAppsNames'))))]" + }, + "existing": true, + "type": "Microsoft.Web/sites/config", + "apiVersion": "2022-09-01", + "name": "[format('{0}/{1}', variables('webAppsNames')[range(0, length(variables('webAppsNames')))[copyIndex()]], 'appsettings')]", + "dependsOn": [ + "[format('webApps[{0}]', range(0, length(variables('webAppsNames')))[copyIndex()])]" + ] + } + } +} \ No newline at end of file diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.bicep b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.bicep new file mode 100644 index 00000000000..8f98a5f9dad --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.bicep @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3120 +// Based on work contributed by @GABRIELNGBTUC + +import * as child from './Tests.Bicep.1.child.bicep' + +var v1 = [] +var v2 = [ + 2 +] + +func getV3() array => union(v1, v2) + +output o1 array = getV3() +output o2 array = child.getV3() +output o3 array = union(child.v1, child.v2) +output o4 array = union(v2, child.v2) +output o5 array = child.getV4() diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child.bicep b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child.bicep new file mode 100644 index 00000000000..5eb32fe19a2 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child.bicep @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +@export() +var v1 = [] + +@export() +var v2 = [ + 1 +] + +@export() +func getV3() array => union(v1, v2) + +import * as child2 from './Tests.Bicep.1.child2.bicep' + +@export() +func getV4() array => union(child2.v1, child2.v2) diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child2.bicep b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child2.bicep new file mode 100644 index 00000000000..096347dcbfe --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child2.bicep @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +@export() +var v1 = [] + +@export() +var v2 = [ + 3 +] diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.json b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.json new file mode 100644 index 00000000000..31c436d8804 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "3104036790319433669" + } + }, + "functions": [ + { + "namespace": "__bicep", + "members": { + "getV3": { + "parameters": [], + "output": { + "type": "array", + "value": "[union(variables('v1'), variables('v2'))]" + } + } + } + }, + { + "namespace": "_1", + "members": { + "getV3": { + "parameters": [], + "output": { + "type": "array", + "value": "[union(variables('_1.v1'), variables('_1.v2'))]" + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "Tests.Bicep.1.child.bicep" + } + } + }, + "getV4": { + "parameters": [], + "output": { + "type": "array", + "value": "[union(variables('_2.v1'), variables('_2.v2'))]" + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "Tests.Bicep.1.child.bicep" + } + } + } + } + } + ], + "variables": { + "v1": [], + "v2": [ + 2 + ], + "_1.v1": [], + "_1.v2": [ + 1 + ], + "_2.v1": [], + "_2.v2": [ + 3 + ] + }, + "resources": {}, + "outputs": { + "o1": { + "type": "array", + "value": "[__bicep.getV3()]" + }, + "o2": { + "type": "array", + "value": "[_1.getV3()]" + }, + "o3": { + "type": "array", + "value": "[union(variables('_1.v1'), variables('_1.v2'))]" + }, + "o4": { + "type": "array", + "value": "[union(variables('v2'), variables('_1.v2'))]" + }, + "o5": { + "type": "array", + "value": "[_1.getV4()]" + } + } +} \ No newline at end of file diff --git a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj index c1f502c73c2..89e3ac05b67 100644 --- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj +++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj @@ -293,6 +293,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + diff --git a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs index 39e0fc24097..846829ff55b 100644 --- a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs @@ -1307,6 +1307,15 @@ public void ProcessTemplate_WhenConditionalExistingReference_IgnoresExpand() Assert.Equal("02041802-66a9-0a85-7330-8186e16422c7", actual["name"].Value()); } + /// + /// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3123 + /// + [Fact] + public void ProcessTemplate_WhenExistingReferenceNameUsesExpression_ShouldExpandExpression() + { + var resources = ProcessTemplate(GetSourcePath("Bicep/SymbolicNameTestCases/Tests.Bicep.3.json"), null, out _); + } + [Fact] public void ProcessTemplate_WhenParented_ShouldReturnExpectedScope() { @@ -1384,6 +1393,30 @@ public void ProcessTemplate_WhenConditionalSecretParameter_ShouldReturnSecretsPl Assert.Equal("placeholder", actual["properties"]["value"].Value()); } + /// + /// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3120 + /// + [Fact] + public void ProcessTemplate_WhenUserDefinedFunctionReferencesExportedVariables_ShouldFindVariable() + { + _ = ProcessTemplate(GetSourcePath("Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.json"), null, out var templateContext); + + Assert.True(templateContext.RootDeployment.TryOutput("o1", out JObject o1)); + Assert.Equal([2], o1["value"].Values()); + + Assert.True(templateContext.RootDeployment.TryOutput("o2", out JObject o2)); + Assert.Equal([1], o2["value"].Values()); + + Assert.True(templateContext.RootDeployment.TryOutput("o3", out JObject o3)); + Assert.Equal([1], o3["value"].Values()); + + Assert.True(templateContext.RootDeployment.TryOutput("o4", out JObject o4)); + Assert.Equal([2, 1], o4["value"].Values()); + + Assert.True(templateContext.RootDeployment.TryOutput("o5", out JObject o5)); + Assert.Equal([3], o5["value"].Values()); + } + #region Helper methods private static string GetSourcePath(string fileName) diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.38.bicep b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.38.bicep index 45ec9c80a9b..873c5c71ab5 100644 --- a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.38.bicep +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.38.bicep @@ -2,6 +2,7 @@ // Licensed under the MIT License. // Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/2850 +// Based on work contributed by @maythamfahmi // Handle from subscription scope. targetScope = 'subscription'