diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index dcbbe805dc1..fe109161aa2 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -28,7 +28,7 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased -What's changed since v1.30.2: +What's changed since v1.30.3: - Engineering: - Bump development tools to .NET 7.0 SDK by @BernieWhite. @@ -36,6 +36,14 @@ What's changed since v1.30.2: - Bump BenchmarkDotNet to v0.13.9. [#2469](https://github.com/Azure/PSRule.Rules.Azure/pull/2469) +## v1.30.3 + +What's changed since v1.30.2: + +- Bug fixes: + - Fixed nullable parameters for built-in types by @BernieWhite. + [#2488](https://github.com/Azure/PSRule.Rules.Azure/issues/2488) + ## v1.30.2 What's changed since v1.30.1: diff --git a/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs b/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs index 8c49831c8b4..78dad147b87 100644 --- a/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs @@ -11,6 +11,7 @@ using PSRule.Rules.Azure.Configuration; using PSRule.Rules.Azure.Pipeline; using PSRule.Rules.Azure.Resources; +using YamlDotNet.Core.Tokens; namespace PSRule.Rules.Azure.Data.Template { @@ -66,6 +67,7 @@ internal abstract class TemplateVisitor : ResourceManagerVisitor private const string PROPERTY_DEFINITIONS = "definitions"; private const string PROPERTY_REF = "$ref"; private const string PROPERTY_ROOTDEPLOYMENT = "rootDeployment"; + private const string PROPERTY_NULLABLE = "nullable"; internal sealed class TemplateContext : ITemplateContext { @@ -960,7 +962,8 @@ private static bool TryParameter(TemplateContext context, string parameterName, return parameter == null || TryParameterAssignment(context, parameterName, parameter) || TryParameterDefaultValue(context, parameterName, parameter) || - TryParameterDefault(context, parameterName, parameter); + TryParameterDefault(context, parameterName, parameter) || + TryParameterNullable(context, parameterName, parameter); } private static bool TryParameterAssignment(TemplateContext context, string parameterName, JObject parameter) @@ -1003,6 +1006,21 @@ private static bool TryParameterDefault(TemplateContext context, string paramete return true; } + /// + /// Handle cases when the parameter has been marked as nullable. + /// + private static bool TryParameterNullable(TemplateContext context, string parameterName, JObject parameter) + { + if (!parameter.TryBoolProperty(PROPERTY_NULLABLE, out var nullable) || !nullable.HasValue) + return false; + + if (!TryParameterType(context, parameter, out var type)) + throw ThrowTemplateParameterException(parameterName); + + AddParameterFromType(context, parameterName, type.Value, JToken.Parse("null")); + return true; + } + private static bool TryParameterType(ITemplateContext context, JObject parameter, out ParameterType? value) { value = null; 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 12ee951399f..dde46fa9db5 100644 --- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj +++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj @@ -191,6 +191,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs index 803098bf3b3..c31aee47b21 100644 --- a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs @@ -876,6 +876,17 @@ public void ArrayContains() Assert.Equal("Standard", resources[10]["properties"]["pricingTier"].Value()); } + [Fact] + public void NullableParameters() + { + var resources = ProcessTemplate(GetSourcePath("Tests.Bicep.27.json"), null, out _); + Assert.Equal(3, resources.Length); + + var actual = resources[2]; + Assert.Equal("Microsoft.Storage/storageAccounts", actual["type"].Value()); + Assert.Equal("TLS1_2", actual["properties"]["minimumTlsVersion"].Value()); + } + #region Helper methods private static string GetSourcePath(string fileName) diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.27.bicep b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.27.bicep new file mode 100644 index 00000000000..2482bdb849a --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.27.bicep @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +module child 'Tests.Bicep.27.child.bicep' = { + name: 'child' + params: {} +} diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.27.child.bicep b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.27.child.bicep new file mode 100644 index 00000000000..234dab08233 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.27.child.bicep @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +param minTLSVersion string? +// param corsRules corsRule + +// type corsRule = { +// allowedHeaders: string[] +// allowedMethods: string[] +// allowedOrigins: string[] +// exposedHeaders: string[] +// maxAgeInSeconds: int +// }[]? + +resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: 'test' + #disable-next-line no-loc-expr-outside-params + location: resourceGroup().location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + minimumTlsVersion: minTLSVersion ?? 'TLS1_2' + } + + // resource blob 'blobServices' = { + // name: 'default' + // properties: { + // cors: { + // corsRules: corsRules ?? [] + // } + // } + // } +} diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.27.json b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.27.json new file mode 100644 index 00000000000..7950425182b --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.27.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.22.6.54827", + "templateHash": "3111791179159915120" + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "child", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": {}, + "template": { + "$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.22.6.54827", + "templateHash": "965836287794218595" + } + }, + "parameters": { + "minTLSVersion": { + "type": "string", + "nullable": true + } + }, + "resources": { + "storage": { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-01-01", + "name": "test", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "minimumTlsVersion": "[coalesce(parameters('minTLSVersion'), 'TLS1_2')]" + } + } + } + } + } + } + ] +} \ No newline at end of file