diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index abe0ca5715..0e04a27e2d 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -29,6 +29,13 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +- New features: + - Added support for expanding from `.jsonc` parameter files by @BernieWhite. + [#2053](https://github.com/Azure/PSRule.Rules.Azure/issues/2053) + - Previously only parameter files with the `.json` extension where automatically expanded. + - This feature adds support so that JSON parameter files with the `.jsonc` extension are also discovered and expanded. + - No additional configuration is required if expansion of JSON parameter files is enabled. + - To enable parameter file expansion set the `AZURE_PARAMETER_FILE_EXPANSION` configuration option to `true`. - Bug fixes: - Fixed projection of default role authorization property `principalType` by @BernieWhite. [#3163](https://github.com/Azure/PSRule.Rules.Azure/issues/3163) diff --git a/docs/using-templates.md b/docs/using-templates.md index b6edae6f99..421017c05a 100644 --- a/docs/using-templates.md +++ b/docs/using-templates.md @@ -7,8 +7,8 @@ author: BernieWhite PSRule for Azure discovers and analyzes Azure resources contained within template and parameter files. To enable this feature, you need to: -- Enable expansion. -- Link parameter files to templates. +1. Enable expansion. +2. Link parameter files to templates. !!! Abstract This topic covers how you can validate Azure resources within template `.json` files. @@ -24,6 +24,15 @@ configuration: AZURE_PARAMETER_FILE_EXPANSION: true ``` +After enabling expansion, PSRule for Azure will start to discover ARM templates and Bicep module from parameter files. +Supported parameter files must: + +- The use `.parameters.json` or `.parameters.jsonc` suffix. i.e. `deploy.parameters.jsonc`. +- The use either of the following schemas: + - `https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#` OR + - `https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#` +- Must be linked to a template or Bicep module using one of the methods described below. + ## Linking templates PSRule for Azure automatically detects parameter files and uses the following logic to link templates or Bicep modules. @@ -36,7 +45,7 @@ PSRule for Azure automatically detects parameter files and uses the following lo For details on both options continue reading. !!! Tip - Linking templates also applies to Bicep modules when you are using `.json` parameter files. + Linking templates also applies to Bicep modules when you are using JSON parameter files. ### By metadata @@ -123,12 +132,18 @@ When metadata links are not set, PSRule will fallback to use a naming convention PSRule for Azure supports linking by naming convention when: -- Parameter files end with `.parameters.json` linking to ARM templates or Bicep modules. +- Parameter files end with `.parameters.json` or `.parameters.jsonc` linking to ARM templates or Bicep modules. - The parameter file prefix matches the file name of the template or Bicep module. - For example, `azuredeploy.parameters.json` links to `azuredeploy.json` or `azuredeploy.bicep`. -- If both an ARM template and Bicep module exist, the template (`.json`) is preferred. - For example, `azuredeploy.parameters.json` chooses `azuredeploy.json` over `azuredeploy.bicep` if both exist. + For example, `azuredeploy.parameters.json` links to `azuredeploy.json`, `azuredeploy.jsonc`, or `azuredeploy.bicep`. - Both parameter file and template or Bicep module must be in the same directory. +- The following search order is used to determine the file that will be used if multiple template or Bicep files exist: + 1. ARM template file with the `.json` extension. + 2. ARM template file with the `.jsonc` extension. + 3. Bicep module file with the `.bicep` extension. + +For example, `azuredeploy.parameters.json` links to `azuredeploy.json` over `azuredeploy.jsonc` or `azuredeploy.bicep`. +When `azuredeploy.json` does not exist, `azuredeploy.jsonc` is chosen. +Finally `azuredeploy.parameters.json` will link to `azuredeploy.bicep` if neither of the other files exist. The following is not currently supported: diff --git a/src/PSRule.Rules.Azure/Data/Template/TemplateLinkHelper.cs b/src/PSRule.Rules.Azure/Data/Template/TemplateLinkHelper.cs index 1f383985a5..7e14256041 100644 --- a/src/PSRule.Rules.Azure/Data/Template/TemplateLinkHelper.cs +++ b/src/PSRule.Rules.Azure/Data/Template/TemplateLinkHelper.cs @@ -24,6 +24,7 @@ internal sealed class TemplateLinkHelper private const string PARAMETER_FILE_SUFFIX = ".parameters"; private const string TEMPLATE_FILE_EXTENSION_JSON = ".json"; + private const string TEMPLATE_FILE_EXTENSION_JSONC = ".jsonc"; private const string TEMPLATE_FILE_EXTENSION_BICEP = ".bicep"; private const char SLASH = '/'; @@ -180,11 +181,20 @@ private static bool TryTemplateByName(string suffix, string parameterFile, out s var baseFilename = filename.Remove(filename.Length - suffix.Length); var jsonTemplateFile = Path.Combine(parentPath, string.Concat(baseFilename, TEMPLATE_FILE_EXTENSION_JSON)); + var jsoncTemplateFile = Path.Combine(parentPath, string.Concat(baseFilename, TEMPLATE_FILE_EXTENSION_JSONC)); var bicepTemplateFile = Path.Combine(parentPath, string.Concat(baseFilename, TEMPLATE_FILE_EXTENSION_BICEP)); if (File.Exists(jsonTemplateFile)) + { templateFile = jsonTemplateFile; + } + else if (File.Exists(jsoncTemplateFile)) + { + templateFile = jsoncTemplateFile; + } else if (File.Exists(bicepTemplateFile)) + { templateFile = bicepTemplateFile; + } return templateFile != null; } diff --git a/src/PSRule.Rules.Azure/rules/Conventions.Rule.ps1 b/src/PSRule.Rules.Azure/rules/Conventions.Rule.ps1 index de501198df..4600a3b619 100644 --- a/src/PSRule.Rules.Azure/rules/Conventions.Rule.ps1 +++ b/src/PSRule.Rules.Azure/rules/Conventions.Rule.ps1 @@ -85,7 +85,7 @@ Export-PSRuleConvention 'Azure.Context' -Initialize { } # Synopsis: Expand Azure resources from parameter files. -Export-PSRuleConvention 'Azure.ExpandTemplate' -If { $Configuration.AZURE_PARAMETER_FILE_EXPANSION -eq $True -and $TargetObject.Extension -eq '.json' -and $Assert.HasJsonSchema($PSRule.GetContentFirstOrDefault($TargetObject), @( +Export-PSRuleConvention 'Azure.ExpandTemplate' -If { $Configuration.AZURE_PARAMETER_FILE_EXPANSION -eq $True -and ($TargetObject.Extension -eq '.json' -or $TargetObject.Extension -eq '.jsonc') -and $Assert.HasJsonSchema($PSRule.GetContentFirstOrDefault($TargetObject), @( "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json`#" "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json`#" ), $True) } -Begin { diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.Cosmos.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.Cosmos.Tests.ps1 index 447f272a54..2fed0b2784 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.Cosmos.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.Cosmos.Tests.ps1 @@ -173,7 +173,7 @@ Describe 'Azure.Cosmos' -Tag 'Cosmos', 'CosmosDB' { Context 'With template' { BeforeAll { - $templatePath = Join-Path -Path $here -ChildPath 'Resources.Cosmos.Parameters.*.json'; + $templatePath = Join-Path -Path $here -ChildPath 'Resources.Cosmos.Parameters.*.json?'; $invokeParams = @{ Baseline = 'Azure.All' Option = (Join-Path -Path $here -ChildPath 'test-template-options.yaml') 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 12bdc22448..9720166653 100644 --- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj +++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj @@ -176,6 +176,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Rules.Azure.Tests/Resources.Cosmos.Parameters.2.json b/tests/PSRule.Rules.Azure.Tests/Resources.Cosmos.Parameters.2.json deleted file mode 100644 index 8f7a8608e2..0000000000 --- a/tests/PSRule.Rules.Azure.Tests/Resources.Cosmos.Parameters.2.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "template": "./Resources.Cosmos.Template.json" - }, - "parameters": { - "accountName": { - "value": "gremlin-002" - } - } -} diff --git a/tests/PSRule.Rules.Azure.Tests/Resources.Cosmos.Parameters.2.jsonc b/tests/PSRule.Rules.Azure.Tests/Resources.Cosmos.Parameters.2.jsonc new file mode 100644 index 0000000000..41734b710c --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Resources.Cosmos.Parameters.2.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "template": "./Resources.Cosmos.Template.json" + }, + "parameters": { + // The account name of the CosmosDB account. + "accountName": { + "value": "gremlin-002" + } + } +} diff --git a/tests/PSRule.Rules.Azure.Tests/TemplateLinkTests.cs b/tests/PSRule.Rules.Azure.Tests/TemplateLinkTests.cs index 2fb13e715f..b40b68e220 100644 --- a/tests/PSRule.Rules.Azure.Tests/TemplateLinkTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/TemplateLinkTests.cs @@ -23,7 +23,7 @@ public void Pipeline() } [Fact] - public void GetBicepParameters() + public void ProcessParameterFile_WhenUsingReferencingBicep_ShouldLinkToBicep() { var helper = new TemplateLinkHelper(GetContext(), AppDomain.CurrentDomain.BaseDirectory, true); @@ -52,6 +52,23 @@ public void GetBicepParameters() Assert.EndsWith("Tests.Bicep.2.parameters.json", link.ParameterFile); } + /// + /// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/2053. + /// + [Fact] + public void ProcessParameterFile_WhenUsingJSONCExtension_ShouldLinkToTemplate() + { + var helper = new TemplateLinkHelper(GetContext(), AppDomain.CurrentDomain.BaseDirectory, true); + + // From naming convention + var link = helper.ProcessParameterFile(GetSourcePath("azuredeploy.parameters.jsonc")); + Assert.NotNull(link); + Assert.NotNull(link.TemplateFile); + Assert.EndsWith("azuredeploy.jsonc", link.TemplateFile); + Assert.NotNull(link.ParameterFile); + Assert.EndsWith("azuredeploy.parameters.jsonc", link.ParameterFile); + } + #region Helper methods private static string GetSourcePath(string fileName) diff --git a/tests/PSRule.Rules.Azure.Tests/azuredeploy.jsonc b/tests/PSRule.Rules.Azure.Tests/azuredeploy.jsonc new file mode 100644 index 0000000000..b073aecf62 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/azuredeploy.jsonc @@ -0,0 +1,105 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "accountName": { + "type": "string", + "metadata": { + "description": "The name of the Cosmos DB account." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location to deploy Azure resources.", + "strongType": "location" + } + }, + "disableKeyBasedMetadataWriteAccess": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "When enabled, resource keys and tokens are limit from performing resource management operations." + } + } + }, + "resources": [ + { + "comments": "A test Cosmos DB account.", + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2021-04-15", + "name": "[parameters('accountName')]", + "location": "[parameters('location')]", + "tags": { + "defaultExperience": "Gremlin (graph)", + "hidden-cosmos-mmspecial": "", + "CosmosAccountType": "Production" + }, + "kind": "GlobalDocumentDB", + "identity": { + "type": "None" + }, + "properties": { + "publicNetworkAccess": "Enabled", + "enableAutomaticFailover": false, + "enableMultipleWriteLocations": false, + "isVirtualNetworkFilterEnabled": false, + "virtualNetworkRules": [], + "disableKeyBasedMetadataWriteAccess": "[parameters('disableKeyBasedMetadataWriteAccess')]", + "enableFreeTier": false, + "enableAnalyticalStorage": false, + "databaseAccountOfferType": "Standard", + "defaultIdentity": "FirstPartyIdentity", + "networkAclBypass": "None", + "consistencyPolicy": { + "defaultConsistencyLevel": "Session", + "maxIntervalInSeconds": 5, + "maxStalenessPrefix": 100 + }, + "locations": [ + { + "locationName": "region", + "provisioningState": "Succeeded", + "failoverPriority": 0, + "isZoneRedundant": true + } + ], + "cors": [], + "capabilities": [ + { + "name": "EnableGremlin" + }, + { + "name": "EnableServerless" + } + ], + "ipRules": [ + { + "ipAddressOrRange": "104.42.195.92" + }, + { + "ipAddressOrRange": "40.76.54.131" + }, + { + "ipAddressOrRange": "52.176.6.30" + }, + { + "ipAddressOrRange": "52.169.50.45" + }, + { + "ipAddressOrRange": "52.187.184.26" + } + ], + "backupPolicy": { + "type": "Periodic", + "periodicModeProperties": { + "backupIntervalInMinutes": 240, + "backupRetentionIntervalInHours": 8 + } + }, + "networkAclBypassResourceIds": [] + } + } + ] +} diff --git a/tests/PSRule.Rules.Azure.Tests/azuredeploy.parameters.jsonc b/tests/PSRule.Rules.Azure.Tests/azuredeploy.parameters.jsonc new file mode 100644 index 0000000000..b04a668446 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/azuredeploy.parameters.jsonc @@ -0,0 +1,10 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // The account name of the CosmosDB account. + "accountName": { + "value": "gremlin-002" + } + } +}