From e56989534df093ba05b9662d9cd2145d638cbf29 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Fri, 15 Jul 2022 18:14:29 +0200 Subject: [PATCH 01/14] Updated Set-ModuleReadMe with Bicep support & regenerated docs --- utilities/tools/Set-ModuleReadMe.ps1 | 873 +++++++++++++++++++++------ 1 file changed, 676 insertions(+), 197 deletions(-) diff --git a/utilities/tools/Set-ModuleReadMe.ps1 b/utilities/tools/Set-ModuleReadMe.ps1 index cefb10a64a..c2f108806b 100644 --- a/utilities/tools/Set-ModuleReadMe.ps1 +++ b/utilities/tools/Set-ModuleReadMe.ps1 @@ -31,7 +31,7 @@ function Set-ResourceTypesSection { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ResourceTypesToExclude', Justification = 'Variable used inside Where-Object block.')] param ( [Parameter(Mandatory)] - [hashtable] $TemplateFileContent, + [Hashtable] $TemplateFileContent, [Parameter(Mandatory)] [object[]] $ReadMeFileContent, @@ -118,7 +118,7 @@ function Set-ParametersSection { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] - [hashtable] $TemplateFileContent, + [Hashtable] $TemplateFileContent, [Parameter(Mandatory)] [object[]] $ReadMeFileContent, @@ -187,8 +187,8 @@ function Set-ParametersSection { } # Add external single quotes to all default values of type string except for those using functions - $defaultValue = ($parameter.defaultValue -is [array]) ? ('[{0}]' -f ($parameter.defaultValue -join ', ')) : (($parameter.defaultValue -is [hashtable]) ? '{object}' : (($parameter.defaultValue -is [string]) -and ($parameter.defaultValue -notmatch '\[\w+\(.*\).*\]') ? '''' + $parameter.defaultValue + '''' : $parameter.defaultValue)) - $allowedValue = ($parameter.allowedValues -is [array]) ? ('[{0}]' -f ($parameter.allowedValues -join ', ')) : (($parameter.allowedValues -is [hashtable]) ? '{object}' : $parameter.allowedValues) + $defaultValue = ($parameter.defaultValue -is [array]) ? ('[{0}]' -f ($parameter.defaultValue -join ', ')) : (($parameter.defaultValue -is [Hashtable]) ? '{object}' : (($parameter.defaultValue -is [string]) -and ($parameter.defaultValue -notmatch '\[\w+\(.*\).*\]') ? '''' + $parameter.defaultValue + '''' : $parameter.defaultValue)) + $allowedValue = ($parameter.allowedValues -is [array]) ? ('[{0}]' -f ($parameter.allowedValues -join ', ')) : (($parameter.allowedValues -is [Hashtable]) ? '{object}' : $parameter.allowedValues) $description = $parameter.metadata.description.Replace("`r`n", '

').Replace("`n", '

') # Update parameter table content based on parameter category @@ -260,7 +260,7 @@ function Set-OutputsSection { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] - [hashtable] $TemplateFileContent, + [Hashtable] $TemplateFileContent, [Parameter(Mandatory)] [object[]] $ReadMeFileContent, @@ -299,6 +299,408 @@ function Set-OutputsSection { return $updatedFileContent } +<# +.SYNOPSIS +Add comments to indicate required & non-required parameters to the given Bicep example + +.DESCRIPTION +Add comments to indicate required & non-required parameters to the given Bicep example. +'Required' is only added if the example has at least one required parameter +'Non-Required' is only added if the example has at least one required parameter and at least one non-required parameter + +.PARAMETER BicepParams +Mandatory. The Bicep parameter block to add the comments to (i.e., should contain everything in between the brackets of a 'params: {...} block) + +.PARAMETER AllParametersList +Mandatory. A list of all top-level (i.e. non-nested) parameter names + +.PARAMETER RequiredParametersList +Mandatory. A list of all required top-level (i.e. non-nested) parameter names + +.EXAMPLE +Add-BicepParameterTypeComment -AllParametersList @('name', 'lock') -RequiredParametersList @('name') -BicepParams "name: 'carml'\nlock: 'CanNotDelete'" + +Add type comments to given bicep params string, using one required parameter 'name'. Would return: + +' + // Required parameters + name: 'carml' + // Non-required parameters + lock: 'CanNotDelete' +' +#> +function Add-BicepParameterTypeComment { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $BicepParams, + + [Parameter(Mandatory = $false)] + [AllowEmptyCollection()] + [string[]] $AllParametersList = @(), + + [Parameter(Mandatory = $false)] + [AllowEmptyCollection()] + [string[]] $RequiredParametersList = @() + ) + + if ($RequiredParametersList.Count -ge 1 -and $AllParametersList.Count -ge 2) { + + $BicepParamsArray = $BicepParams -split '\n' + + # [1/4] Check where the 'last' required parameter is located in the example (and what its indent is) + $parameterToSplitAt = $RequiredParametersList[-1] + $requiredParameterIndent = ([regex]::Match($BicepParamsArray[0], '^(\s+).*')).Captures.Groups[1].Value.Length + + # [1/4] Add a comment where the required parameters start + $BicepParamsArray = @('{0}// Required parameters' -f (' ' * $requiredParameterIndent)) + $BicepParamsArray[(0 .. ($BicepParamsArray.Count))] + + # [1/4] Find the location if the last required parameter + $requiredParameterStartIndex = ($BicepParamsArray | Select-String ('^[\s]{0}{1}:.+' -f "{$requiredParameterIndent}", $parameterToSplitAt) | ForEach-Object { $_.LineNumber - 1 })[0] + + # [1/4] If we have more than only required parameters, let's add a corresponding comment + if ($AllParametersList.Count -gt $RequiredParametersList.Count) { + $nextLineIndent = ([regex]::Match($BicepParamsArray[$requiredParameterStartIndex + 1], '^(\s+).*')).Captures.Groups[1].Value.Length + if ($nextLineIndent -gt $requiredParameterIndent) { + # Case Param is object/array: Search in rest of array for the next closing bracket with the same indent - and then add the search index (1) & initial index (1) count back in + $requiredParameterEndIndex = ($BicepParamsArray[($requiredParameterStartIndex + 1)..($BicepParamsArray.Count)] | Select-String "^[\s]{$requiredParameterIndent}\S+" | ForEach-Object { $_.LineNumber - 1 })[0] + 1 + $requiredParameterStartIndex + } else { + # Case Param is single line bool/string/int: Add an index (1) for the 'required' comment + $requiredParameterEndIndex = $requiredParameterStartIndex + } + + # Add a comment where the non-required parameters start + $BicepParamsArray = $BicepParamsArray[0..$requiredParameterEndIndex] + ('{0}// Non-required parameters' -f (' ' * $requiredParameterIndent)) + $BicepParamsArray[(($requiredParameterEndIndex + 1) .. ($BicepParamsArray.Count))] + } + + return ($BicepParamsArray | Out-String).TrimEnd() + } + + return $BicepParams +} + +<# +.SYNOPSIS +Sort the given JSON paramters into required & non-required parameters, each sorted alphabetically + +.DESCRIPTION +Sort the given JSON paramters into required & non-required parameters, each sorted alphabetically + +.PARAMETER ParametersJSON +Mandatory. The JSON parameters block to process (ideally already without 'value' property) + +.PARAMETER RequiredParametersList +Mandatory. A list of all required top-level (i.e. non-nested) parameter names + +.EXAMPLE +Get-OrderedParametersJSON -RequiredParametersList @('name') -ParametersJSON '{ "diagnosticLogsRetentionInDays": 7,"lock": "CanNotDelete","name": "carml" }' + +Order the given JSON object alphabetically. Would result into: + +@{ + name: 'carml' + diagnosticLogsRetentionInDays: 7 + lock: 'CanNotDelete' +} +#> +function Get-OrderedParametersJSON { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $ParametersJSON, + + [Parameter(Mandatory = $false)] + [AllowEmptyCollection()] + [string[]] $RequiredParametersList = @() + ) + + # Load used function(s) + . (Join-Path $PSScriptRoot 'helper' 'ConvertTo-OrderedHashtable.ps1') + + # [1/3] Get all parameters from the parameter object and order them recursively + $orderedContentInJSONFormat = ConvertTo-OrderedHashtable -JSONInputObject $parametersJSON + + # [2/3] Sort 'required' parameters to the front + $orderedJSONParameters = [ordered]@{} + $orderedTopLevelParameterNames = $orderedContentInJSONFormat.psbase.Keys # We must use PS-Base to handle conflicts of HashTable properties & keys (e.g. for a key 'keys'). + # [2.1] Add required parameters first + $orderedTopLevelParameterNames | Where-Object { $_ -in $RequiredParametersList } | ForEach-Object { $orderedJSONParameters[$_] = $orderedContentInJSONFormat[$_] } + # [2.2] Add rest after + $orderedTopLevelParameterNames | Where-Object { $_ -notin $RequiredParametersList } | ForEach-Object { $orderedJSONParameters[$_] = $orderedContentInJSONFormat[$_] } + + # [3/3] Handle empty dictionaries (in case the parmaeter file was empty) + if ($orderedJSONParameters.count -eq 0) { + $orderedJSONParameters = '' + } + + return $orderedJSONParameters +} + +<# +.SYNOPSIS +Sort the given JSON parameters into a new JSON parameter object, all parameter sorted into required & non-required parameters, each sorted alphabetically + +.DESCRIPTION +Sort the given JSON parameters into a new JSON parameter object, all parameter sorted into required & non-required parameters, each sorted alphabetically. +The location where required & non-required parameters start is highlighted with by a corresponding comment + +.PARAMETER ParametersJSON +Mandatory. The parameter JSON object to process + +.PARAMETER RequiredParametersList +Mandatory. A list of all required top-level (i.e. non-nested) parameter names + +.EXAMPLE +Build-OrderedJSONObject -RequiredParametersList @('name') -ParametersJSON '{ "lock": { "value": "CanNotDelete" }, "name": { "value": "carml" }, "diagnosticLogsRetentionInDays": { "value": 7 } }' + +Build a formatted Parameter-JSON object with one required parameter. Would result into: + +'{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Required parameters + "name": { + "value": "carml" + }, + // Non-required parameters + "diagnosticLogsRetentionInDays": { + "value": 7 + }, + "lock": { + "value": "CanNotDelete" + } + } +}' +#> +function Build-OrderedJSONObject { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $ParametersJSON, + + [Parameter(Mandatory = $false)] + [AllowEmptyCollection()] + [string[]] $RequiredParametersList = @() + ) + + # [1/9] Sort parameter alphabetically + $orderedJSONParameters = Get-OrderedParametersJSON -ParametersJSON $ParametersJSON -RequiredParametersList $RequiredParametersList + + # [2/9] Build the ordered parameter file syntax back up + $jsonExample = ([ordered]@{ + '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' + contentVersion = '1.0.0.0' + parameters = (-not [String]::IsNullOrEmpty($orderedJSONParameters)) ? $orderedJSONParameters : @{} + } | ConvertTo-Json -Depth 99) + + # [3/8] If we have at least one required and one other parameter we want to add a comment + if ($RequiredParametersList.Count -ge 1 -and $OrderedJSONParameters.Keys.Count -ge 2) { + + $jsonExampleArray = $jsonExample -split '\n' + + # [4/8] Check where the 'last' required parameter is located in the example (and what its indent is) + $parameterToSplitAt = $RequiredParametersList[-1] + $parameterStartIndex = ($jsonExampleArray | Select-String '.*"parameters": \{.*' | ForEach-Object { $_.LineNumber - 1 })[0] + $requiredParameterIndent = ([regex]::Match($jsonExampleArray[($parameterStartIndex + 1)], '^(\s+).*')).Captures.Groups[1].Value.Length + + # [5/8] Add a comment where the required parameters start + $jsonExampleArray = $jsonExampleArray[0..$parameterStartIndex] + ('{0}// Required parameters' -f (' ' * $requiredParameterIndent)) + $jsonExampleArray[(($parameterStartIndex + 1) .. ($jsonExampleArray.Count))] + + # [6/8] Find the location if the last required parameter + $requiredParameterStartIndex = ($jsonExampleArray | Select-String "^[\s]{$requiredParameterIndent}`"$parameterToSplitAt`": \{.*" | ForEach-Object { $_.LineNumber - 1 })[0] + + # [7/8] If we have more than only required parameters, let's add a corresponding comment + if ($orderedJSONParameters.Keys.Count -gt $RequiredParametersList.Count ) { + # Search in rest of array for the next closing bracket with the same indent - and then add the search index (1) & initial index (1) count back in + $requiredParameterEndIndex = ($jsonExampleArray[($requiredParameterStartIndex + 1)..($jsonExampleArray.Count)] | Select-String "^[\s]{$requiredParameterIndent}\}" | ForEach-Object { $_.LineNumber - 1 })[0] + 1 + $requiredParameterStartIndex + + # Add a comment where the non-required parameters start + $jsonExampleArray = $jsonExampleArray[0..$requiredParameterEndIndex] + ('{0}// Non-required parameters' -f (' ' * $requiredParameterIndent)) + $jsonExampleArray[(($requiredParameterEndIndex + 1) .. ($jsonExampleArray.Count))] + } + + # [8/8] Convert the processed array back into a string + return $jsonExampleArray | Out-String + } + + return $jsonExample +} + +<# +.SYNOPSIS +Convert the given Bicep parameter block to JSON parameter block + +.DESCRIPTION +Convert the given Bicep parameter block to JSON parameter block + +.PARAMETER BicepParamBlock +Mandatory. The Bicep parameter block to process + +.EXAMPLE +ConvertTo-FormattedJSONParameterObject -BicepParamBlock "name: 'carml'\nlock: 'CanNotDelete'" + +Convert the Bicep string "name: 'carml'\nlock: 'CanNotDelete'" into a parameter JSON object. Would result into: + +@{ + lock = @{ + value = 'carml' + } + lock = @{ + value = 'CanNotDelete' + } +} +#> +function ConvertTo-FormattedJSONParameterObject { + + [CmdletBinding()] + param ( + [Parameter()] + [string] $BicepParamBlock + ) + + # [1/4] Detect top level params for later processing + $bicepParamBlockArray = $BicepParamBlock -split '\n' + $topLevelParamIndent = ([regex]::Match($bicepParamBlockArray[0], '^(\s+).*')).Captures.Groups[1].Value.Length + $topLevelParams = $bicepParamBlockArray | Where-Object { $_ -match "^\s{$topLevelParamIndent}[0-9a-zA-Z]+:.*" } | ForEach-Object { ($_ -split ':')[0].Trim() } + + # [2/4] Add JSON-specific syntax to the Bicep param block to enable us to treat is as such + # [1.1] Syntax: Outer brackets + $paramInJsonFormat = @( + '{', + $BicepParamBlock + '}' + ) | Out-String + + # [1.2] Syntax: All single-quotes are double-quotes + $paramInJsonFormat = $paramInJsonFormat -replace "'", '"' + # [1.3] Syntax: Everything left of a ':' should be wrapped in quotes (as a parameter name is always a string) + $paramInJsonFormat = $paramInJsonFormat -replace '([0-9a-zA-Z]+):', '"$1":' + + # [1.4] Split the object to format line-by-line (& also remove any empty lines) + $paramInJSONFormatArray = $paramInJsonFormat -split '\n' | Where-Object { $_ } + + # [1.5] Syntax: Replace Bicep resource ID references + for ($index = 0; $index -lt $paramInJSONFormatArray.Count; $index++) { + if ($paramInJSONFormatArray[$index] -like '*:*' -and ($paramInJSONFormatArray[$index] -split ':')[1].Trim() -notmatch '".+"' -and $paramInJSONFormatArray[$index] -like '*.*') { + # In case of a reference like : "virtualWanId": resourceGroupResources.outputs.virtualWWANResourceId + $paramInJSONFormatArray[$index] = '{0}: "<{1}>"' -f ($paramInJSONFormatArray[$index] -split ':')[0], ([regex]::Match(($paramInJSONFormatArray[$index] -split ':')[0], '"(.+)"')).Captures.Groups[1].Value + } + if ($paramInJSONFormatArray[$index] -notlike '*:*' -and $paramInJSONFormatArray[$index] -notlike '*"*"*' -and $paramInJSONFormatArray[$index] -like '*.*') { + # In case of a reference like : [ \n resourceGroupResources.outputs.managedIdentityPrincipalId \n ] + $paramInJSONFormatArray[$index] = '"<{0}>"' -f $paramInJSONFormatArray[$index].Split('.')[-1].Trim() + } + } + + # [1.6] Syntax: Add comma everywhere unless: + # - the current line has an opening 'object: {' or 'array: [' character + # - the line after the current line has a closing 'object: {' or 'array: [' character + # - it's the last closing bracket + for ($index = 0; $index -lt $paramInJSONFormatArray.Count; $index++) { + if (($paramInJSONFormatArray[$index] -match '[\{|\[]') -or (($index -lt $paramInJSONFormatArray.Count - 1) -and $paramInJSONFormatArray[$index + 1] -match '[\]|\}]') -or ($index -eq $paramInJSONFormatArray.Count - 1)) { + continue + } + $paramInJSONFormatArray[$index] = '{0},' -f $paramInJSONFormatArray[$index].Trim() + } + + # [1.7] Format the final JSON string to an object to enable processing + $paramInJsonFormatObject = $paramInJSONFormatArray | Out-String | ConvertFrom-Json -AsHashtable -Depth 99 + + # [3/4] Inject top-level 'value`' properties + $paramInJsonFormatObjectWithValue = @{} + foreach ($paramKey in $topLevelParams) { + $paramInJsonFormatObjectWithValue[$paramKey] = @{ + value = $paramInJsonFormatObject[$paramKey] + } + } + + # [4/4] Return result + return $paramInJsonFormatObjectWithValue +} + +<# +.SYNOPSIS +Convert the given parameter JSON object into a formatted Bicep object (i.e., sorted & with required/non-required comments) + +.DESCRIPTION +Convert the given parameter JSON object into a formatted Bicep object (i.e., sorted & with required/non-required comments) + +.PARAMETER JSONParameters +Mandatory. The parameter JSON object to process. + +.PARAMETER RequiredParametersList +Mandatory. A list of all required top-level (i.e. non-nested) parameter names + +.EXAMPLE +ConvertTo-FormattedBicep -RequiredParametersList @('name') -JSONParameters @{ lock = @{ value = 'carml' }; lock = @{ value = 'CanNotDelete' } } + +Convert the given JSONParameters object with one required parameter to a formatted Bicep object. Would result into: + +' + // Required parameters + name: 'carml' + // Non-required parameters + diagnosticLogsRetentionInDays: 7 + lock: 'CanNotDelete' +' +#> +function ConvertTo-FormattedBicep { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Hashtable] $JSONParameters, + + [Parameter(Mandatory = $false)] + [AllowEmptyCollection()] + [string[]] $RequiredParametersList = @() + ) + + # Remove 'value' parameter property, if any (e.g. when dealing with a classic parameter file) + $JSONParametersWithoutValue = @{} + foreach ($parameter in $JSONParameters.psbase.Keys) { + if ($JSONParameters[$parameter].Keys -eq 'value') { + $JSONParametersWithoutValue[$parameter] = $JSONParameters.$parameter.value + } else { + $JSONParametersWithoutValue[$parameter] = $JSONParameters.$parameter + } + } + + # [1/4] Order parameters recursively + if ($JSONParametersWithoutValue.Keys.Count -gt 0) { + $orderedJSONParameters = Get-OrderedParametersJSON -ParametersJSON ($JSONParametersWithoutValue | ConvertTo-Json -Depth 99) -RequiredParametersList $RequiredParametersList + } else { + $orderedJSONParameters = @{} + } + # [2/4] Remove any JSON specific formatting + $templateParameterObject = $orderedJSONParameters | ConvertTo-Json -Depth 99 + if ($templateParameterObject -ne '{}') { + $contentInBicepFormat = $templateParameterObject -replace '"', "'" # Update any [xyz: "xyz"] to [xyz: 'xyz'] + $contentInBicepFormat = $contentInBicepFormat -replace ',', '' # Update any [xyz: xyz,] to [xyz: xyz] + $contentInBicepFormat = $contentInBicepFormat -replace "'(\w+)':", '$1:' # Update any ['xyz': xyz] to [xyz: xyz] + $contentInBicepFormat = $contentInBicepFormat -replace "'(.+.getSecret\('.+'\))'", '$1' # Update any [xyz: 'xyz.GetSecret()'] to [xyz: xyz.GetSecret()] + + $bicepParamsArray = $contentInBicepFormat -split '\n' + $bicepParamsArray = $bicepParamsArray[1..($bicepParamsArray.count - 2)] + } + + # [3/4] Format params with indent + $BicepParams = ($bicepParamsArray | ForEach-Object { " $_" } | Out-String).TrimEnd() + + # [4/4] Add comment where required & optional parameters start + $splitInputObject = @{ + BicepParams = $BicepParams + RequiredParametersList = $RequiredParametersList + AllParametersList = $JSONParametersWithoutValue.Keys + } + $commentedBicepParams = Add-BicepParameterTypeComment @splitInputObject + + return $commentedBicepParams +} + <# .SYNOPSIS Generate 'Deployment examples' for the ReadMe out of the parameter files currently used to test the template @@ -312,6 +714,9 @@ Mandatory. The path to the template file .PARAMETER TemplateFileContent Mandatory. The template file content object to crawl data from +.PARAMETER TemplateFilePath +Mandatory. The path to the template file + .PARAMETER ReadMeFileContent Mandatory. The readme file content array to update @@ -324,8 +729,11 @@ Optional. A switch to control whether or not to add a ARM-JSON-Parameter file ex .PARAMETER addBicep Optional. A switch to control whether or not to add a Bicep deployment example. Defaults to true. +.PARAMETER ProjectSettings +Optional. Projects settings to draw information from. For example the `namePrefix`. + .EXAMPLE -Set-DeploymentExamplesSection -TemplateFilePath 'C:/deploy.bicep' -TemplateFileContent @{ resource = @{}; ... } -ReadMeFileContent @('# Title', '', '## Section 1', ...) +Set-DeploymentExamplesSection -TemplateFileContent @{ resource = @{}; ... } -TemplateFilePath 'C:/deploy.bicep' -ReadMeFileContent @('# Title', '', '## Section 1', ...) Update the given readme file's 'Deployment Examples' section based on the given template file content #> @@ -337,7 +745,7 @@ function Set-DeploymentExamplesSection { [string] $TemplateFilePath, [Parameter(Mandatory)] - [hashtable] $TemplateFileContent, + [Hashtable] $TemplateFileContent, [Parameter(Mandatory = $true)] [object[]] $ReadMeFileContent, @@ -348,13 +756,13 @@ function Set-DeploymentExamplesSection { [Parameter(Mandatory = $false)] [bool] $addBicep = $true, + [Parameter(Mandatory = $false)] + [Hashtable] $ProjectSettings = @{}, + [Parameter(Mandatory = $false)] [string] $SectionStartIdentifier = '## Deployment examples' ) - # Load used function(s) - . (Join-Path $PSScriptRoot 'helper' 'ConvertTo-OrderedHashtable.ps1') - # Process content $SectionContent = [System.Collections.ArrayList]@( 'The following module usage examples are retrieved from the content of the files hosted in the module''s `.test` folder.', @@ -366,17 +774,21 @@ function Set-DeploymentExamplesSection { $moduleRoot = Split-Path $TemplateFilePath -Parent $resourceTypeIdentifier = $moduleRoot.Replace('\', '/').Split('/modules/')[1].TrimStart('/') $resourceType = $resourceTypeIdentifier.Split('/')[1] - $parameterFiles = Get-ChildItem (Join-Path $moduleRoot '.test') -Filter '*parameters.json' -Recurse + $testFilePaths = (Get-ChildItem (Join-Path -Path $moduleRoot -ChildPath '.test') -File).FullName | Where-Object { $_ -match '.+\.[bicep|json]' } - $requiredParameterNames = $TemplateFileContent.parameters.Keys | Where-Object { $TemplateFileContent.parameters[$_].Keys -notcontains 'defaultValue' } | Sort-Object + $RequiredParametersList = $TemplateFileContent.parameters.Keys | Where-Object { $TemplateFileContent.parameters[$_].Keys -notcontains 'defaultValue' } | Sort-Object ############################ ## Process test files ## ############################ $pathIndex = 1 - foreach ($testFilePath in $parameterFiles.FullName) { - $contentInJSONFormat = Get-Content -Path $testFilePath -Encoding 'utf8' | Out-String + foreach ($testFilePath in $testFilePaths) { + # Read content + $rawContentArray = Get-Content -Path $testFilePath + $rawContent = Get-Content -Path $testFilePath -Encoding 'utf8' | Out-String + + # Format example header $exampleTitle = ((Split-Path $testFilePath -LeafBase) -replace '\.', ' ') -replace ' parameters', '' $TextInfo = (Get-Culture).TextInfo $exampleTitle = $TextInfo.ToTitleCase($exampleTitle) @@ -384,216 +796,271 @@ function Set-DeploymentExamplesSection { '

Example {0}: {1}

' -f $pathIndex, $exampleTitle ) - if ($addBicep) { - $JSONParametersHashTable = (ConvertFrom-Json $contentInJSONFormat -AsHashtable -Depth 99).parameters - - # Handle KeyVaut references - $keyVaultReferences = $JSONParametersHashTable.Keys | Where-Object { $JSONParametersHashTable[$_].Keys -contains 'reference' } - - if ($keyVaultReferences.Count -gt 0) { - $keyVaultReferenceData = @() - foreach ($reference in $keyVaultReferences) { - $resourceIdElem = $JSONParametersHashTable[$reference].reference.keyVault.id -split '/' - $keyVaultReferenceData += @{ - subscriptionId = $resourceIdElem[2] - resourceGroupName = $resourceIdElem[4] - vaultName = $resourceIdElem[-1] - secretName = $JSONParametersHashTable[$reference].reference.secretName - parameterName = $reference - } - } + ## ----------------------------------- ## + ## Handle by type (Bicep vs. JSON) ## + ## ----------------------------------- ## + if ((Split-Path $testFilePath -Extension) -eq '.bicep') { + + # ------------------------- # + # Prepare Bicep to JSON # + # ------------------------- # + + # [1/6] Search for the relevant parameter start & end index + $bicepTestStartIndex = $rawContentArray.IndexOf("module testDeployment '../deploy.bicep' = {") + + $bicepTestEndIndex = $bicepTestStartIndex + do { + $bicepTestEndIndex++ + } while ($rawContentArray[$bicepTestEndIndex] -ne '}') + + $rawBicepExample = $rawContentArray[$bicepTestStartIndex..$bicepTestEndIndex] + + # [2/6] Replace placeholders + $namePrefix = ($ProjectSettings.parameterFileTokens.localTokens | Where-Object { $_.name -eq 'namePrefix' }).value + $serviceShort = ([regex]::Match($rawContent, "(?m)^param serviceShort string = '(.+)'\s*$")).Captures.Groups[1].Value + + $rawBicepExampleString = ($rawBicepExample | Out-String) + $rawBicepExampleString = $rawBicepExampleString -replace '\$\{serviceShort\}', $serviceShort + $rawBicepExampleString = $rawBicepExampleString -replace '\$\{namePrefix\}', $namePrefix + $rawBicepExampleString = $rawBicepExampleString -replace '(?m):\s*location\s*$', ': ''''' + + # [3/6] Format header, remove scope property & any empty line + $rawBicepExample = $rawBicepExampleString -split '\n' + $rawBicepExample[0] = "module $resourceType './$resourceTypeIdentifier/deploy.bicep = {" + $rawBicepExample = $rawBicepExample | Where-Object { $_ -notmatch 'scope: *' } | Where-Object { -not [String]::IsNullOrEmpty($_) } + + # [4/6] Extract param block + $rawBicepExampleArray = $rawBicepExample -split '\n' + $moduleDeploymentPropertyIndent = ([regex]::Match($rawBicepExampleArray[1], '^(\s+).*')).Captures.Groups[1].Value.Length + $paramsStartIndex = ($rawBicepExampleArray | Select-String ("^[\s]{$moduleDeploymentPropertyIndent}params:[\s]*\{") | ForEach-Object { $_.LineNumber - 1 })[0] + 1 + $paramsEndIndex = ($rawBicepExampleArray[($paramsStartIndex + 1)..($rawBicepExampleArray.Count)] | Select-String "^[\s]{$moduleDeploymentPropertyIndent}\}" | ForEach-Object { $_.LineNumber - 1 })[0] + $paramsStartIndex + $paramBlock = ($rawBicepExampleArray[$paramsStartIndex..$paramsEndIndex] | Out-String).TrimEnd() + + # [5/6] Convert Bicep parameter block to JSON parameter block to enable processing + $conversionInputObject = @{ + BicepParamBlock = $paramBlock } + $paramsInJSONFormat = ConvertTo-FormattedJSONParameterObject @conversionInputObject - $extendedKeyVaultReferences = @() - $counter = 0 - foreach ($reference in ($keyVaultReferenceData | Sort-Object -Property 'vaultName' -Unique)) { - $counter++ - $extendedKeyVaultReferences += @( - "resource kv$counter 'Microsoft.KeyVault/vaults@2019-09-01' existing = {", - (" name: '{0}'" -f $reference.vaultName), - (" scope: resourceGroup('{0}','{1}')" -f $reference.subscriptionId, $reference.resourceGroupName), - '}', - '' - ) - - # Add attribute for later correct reference - $keyVaultReferenceData | Where-Object { $_.vaultName -eq $reference.vaultName } | ForEach-Object { - $_['vaultResourceReference'] = "kv$counter" - } - } - - # Handle VALUE references (i.e. remove them) - $JSONParameters = (ConvertFrom-Json $contentInJSONFormat -Depth 99 -AsHashtable).parameters - $JSONParametersWithoutValue = @{} - foreach ($parameterName in $JSONParameters.Keys) { - if ($JSONParameters[$parameterName].Keys -eq 'value') { - $JSONParametersWithoutValue[$parameterName] = $JSONParameters[$parameterName]['value'] - } else { - # replace key vault references - $matchingTuple = $keyVaultReferenceData | Where-Object { $_.parameterName -eq $parameterName } - $JSONParametersWithoutValue[$parameterName] = "{0}.getSecret('{1}')" -f $matchingTuple.vaultResourceReference, $matchingTuple.secretName - } + # [6/6] Convert JSON parameters back to Bicep and order & format them + $conversionInputObject = @{ + JSONParameters = $paramsInJSONFormat + RequiredParametersList = $RequiredParametersList } + $bicepExample = ConvertTo-FormattedBicep @conversionInputObject - # Order parameters recursively - $JSONParametersWithoutValue = ConvertTo-OrderedHashtable -JSONInputObject ($JSONParametersWithoutValue | ConvertTo-Json -Depth 99) + # --------------------- # + # Add Bicep example # + # --------------------- # + if ($addBicep) { - # Sort 'required' parameters to the front - $orderedJSONParameters = [ordered]@{} - $orderedTopLevelParameterNames = $JSONParametersWithoutValue.psbase.Keys # We must use PS-Base to handle conflicts of HashTable properties & keys (e.g. for a key 'keys'). - # Add required parameters first - $orderedTopLevelParameterNames | Where-Object { $_ -in $requiredParameterNames } | ForEach-Object { $orderedJSONParameters[$_] = $JSONParametersWithoutValue[$_] } - # Add rest after - $orderedTopLevelParameterNames | Where-Object { $_ -notin $requiredParameterNames } | ForEach-Object { $orderedJSONParameters[$_] = $JSONParametersWithoutValue[$_] } + $formattedBicepExample = $rawBicepExample[0..($paramsStartIndex - 1)] + ($bicepExample -split '\n') + $rawBicepExample[($paramsEndIndex + 1)..($rawBicepExample.Count)] - if ($orderedJSONParameters.count -eq 0) { - # Handle empty dictionaries (in case the parmaeter file was empty) - $orderedJSONParameters = @{} - } - - $templateParameterObject = $orderedJSONParameters | ConvertTo-Json -Depth 99 - if ($templateParameterObject -ne '{}') { - $contentInBicepFormat = $templateParameterObject -replace '"', "'" # Update any [xyz: "xyz"] to [xyz: 'xyz'] - $contentInBicepFormat = $contentInBicepFormat -replace ',', '' # Update any [xyz: xyz,] to [xyz: xyz] - $contentInBicepFormat = $contentInBicepFormat -replace "'(\w+)':", '$1:' # Update any ['xyz': xyz] to [xyz: xyz] - $contentInBicepFormat = $contentInBicepFormat -replace "'(.+.getSecret\('.+'\))'", '$1' # Update any [xyz: 'xyz.GetSecret()'] to [xyz: xyz.GetSecret()] - - $bicepParamsArray = $contentInBicepFormat -split '\n' - $bicepParamsArray = $bicepParamsArray[1..($bicepParamsArray.count - 2)] - } - - # Format params with indent - $bicepExample = $bicepParamsArray | ForEach-Object { " $_" } - - # Optional: Add comment where required & optional parameters start - # ---------------------------------------------------------------- - if ($requiredParameterNames -is [string]) { - $requiredParameterNames = @($requiredParameterNames) + $SectionContent += @( + '', + '
' + '' + 'via Bicep module' + '' + '```bicep', + ($formattedBicepExample | ForEach-Object { "$_" }).TrimEnd(), + '```', + '', + '
', + '

' + ) } - # If we have at least one required and one other parameter we want to add a comment - if ($requiredParameterNames.Count -ge 1 -and $orderedJSONParameters.Keys.Count -ge 2) { - - $bicepExampleArray = $bicepExample -split '\n' - - # Check where the 'last' required parameter is located in the example (and what its indent is) - $parameterToSplitAt = $requiredParameterNames[-1] - $requiredParameterIndent = ([regex]::Match($bicepExampleArray[0], '^(\s+).*')).Captures.Groups[1].Value.Length - - # Add a comment where the required parameters start - $bicepExampleArray = @('{0}// Required parameters' -f (' ' * $requiredParameterIndent)) + $bicepExampleArray[(0 .. ($bicepExampleArray.Count))] - - # Find the location if the last required parameter - $requiredParameterStartIndex = ($bicepExampleArray | Select-String ('^[\s]{0}{1}:.+' -f "{$requiredParameterIndent}", $parameterToSplitAt) | ForEach-Object { $_.LineNumber - 1 })[0] - - # If we have more than only required parameters, let's add a corresponding comment - if ($orderedJSONParameters.Keys.Count -gt $requiredParameterNames.Count) { - $nextLineIndent = ([regex]::Match($bicepExampleArray[$requiredParameterStartIndex + 1], '^(\s+).*')).Captures.Groups[1].Value.Length - if ($nextLineIndent -gt $requiredParameterIndent) { - # Case Param is object/array: Search in rest of array for the next closing bracket with the same indent - and then add the search index (1) & initial index (1) count back in - $requiredParameterEndIndex = ($bicepExampleArray[($requiredParameterStartIndex + 1)..($bicepExampleArray.Count)] | Select-String "^[\s]{$requiredParameterIndent}\S+" | ForEach-Object { $_.LineNumber - 1 })[0] + 1 + $requiredParameterStartIndex - } else { - # Case Param is single line bool/string/int: Add an index (1) for the 'required' comment - $requiredParameterEndIndex = $requiredParameterStartIndex - } + # -------------------- # + # Add JSON example # + # -------------------- # + if ($addJson) { - # Add a comment where the non-required parameters start - $bicepExampleArray = $bicepExampleArray[0..$requiredParameterEndIndex] + ('{0}// Non-required parameters' -f (' ' * $requiredParameterIndent)) + $bicepExampleArray[(($requiredParameterEndIndex + 1) .. ($bicepExampleArray.Count))] + # [2/3] Get all parameters from the parameter object and order them recursively + $orderingInputObject = @{ + ParametersJSON = $paramsInJSONFormat | ConvertTo-Json -Depth 99 + RequiredParametersList = $RequiredParametersList } + $orderedJSONExample = Build-OrderedJSONObject @orderingInputObject - $bicepExample = $bicepExampleArray | Out-String + # [3/3] Create the final content block + $SectionContent += @( + '', + '

' + '' + 'via JSON Parameter file' + '' + '```json', + $orderedJSONExample.Trim() + '```', + '', + '
', + '

' + ) } + } else { + # ------------------------- # + # Prepare JSON to Bicep # + # ------------------------- # - $SectionContent += @( - '', - '

' - '' - 'via Bicep module' - '' - '```bicep', - $extendedKeyVaultReferences, - "module $resourceType './$resourceTypeIdentifier/deploy.bicep' = {" - " name: '`${uniqueString(deployment().name)}-$resourceType'" - ' params: {' - $bicepExample.TrimEnd(), - ' }' - '}' - '```', - '', - '
' - '

' - ) - } - - if ($addJson) { - $orderedContentInJSONFormat = ConvertTo-OrderedHashtable -JSONInputObject (($contentInJSONFormat | ConvertFrom-Json).parameters | ConvertTo-Json -Depth 99) - - # Sort 'required' parameters to the front - $orderedJSONParameters = [ordered]@{} - $orderedTopLevelParameterNames = $orderedContentInJSONFormat.psbase.Keys # We must use PS-Base to handle conflicts of HashTable properties & keys (e.g. for a key 'keys'). - # Add required parameters first - $orderedTopLevelParameterNames | Where-Object { $_ -in $requiredParameterNames } | ForEach-Object { $orderedJSONParameters[$_] = $orderedContentInJSONFormat[$_] } - # Add rest after - $orderedTopLevelParameterNames | Where-Object { $_ -notin $requiredParameterNames } | ForEach-Object { $orderedJSONParameters[$_] = $orderedContentInJSONFormat[$_] } + $rawContentHashtable = $rawContent | ConvertFrom-Json -Depth 99 -AsHashtable -NoEnumerate - if ($orderedJSONParameters.count -eq 0) { - # Handle empty dictionaries (in case the parmaeter file was empty) - $orderedJSONParameters = '' - } + # First we need to check if we're dealing with classic JSON-Parameter file, or a deployment test file (which contains resource deployments & parameters) + $isParameterFile = $rawContentHashtable.'$schema' -like '*deploymentParameters*' + if (-not $isParameterFile) { + # Case 1: Uses deployment test file (instead of parameter file). + # [1/3] Need to extract parameters. The taarget is to get an object which 1:1 represents a classic JSON-Parameter file (aside from KeyVault references) + $testResource = $rawContentHashtable.resources | Where-Object { $_.name -like '*-test-*' } - $jsonExample = ([ordered]@{ + # [2/3] Build the full ARM-JSON parameter file + $jsonParameterContent = [ordered]@{ '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' contentVersion = '1.0.0.0' - parameters = (-not [String]::IsNullOrEmpty($orderedJSONParameters)) ? $orderedJSONParameters : @{} - } | ConvertTo-Json -Depth 99) + parameters = $testResource.properties.parameters + } + $jsonParameterContent = ($jsonParameterContent | ConvertTo-Json -Depth 99).TrimEnd() + + # [3/3] Remove 'externalResourceReferences' that are generated for Bicep's 'existing' resource references. Removing them will make the file more readable + $jsonParameterContentArray = $jsonParameterContent -split '\n' + foreach ($row in ($jsonParameterContentArray | Where-Object { $_ -like '*reference(extensionResourceId*' })) { + $expectedValue = ([regex]::Match($row, '.+\[reference\(extensionResourceId.+\.(.+)\.value\]"')).Captures.Groups[1].Value + $toReplaceValue = ([regex]::Match($row, '"(\[reference\(extensionResourceId.+)"')).Captures.Groups[1].Value - # Optional: Add comment where required & optional parameters start - # ---------------------------------------------------------------- - if ($requiredParameterNames -is [string]) { - $requiredParameterNames = @($requiredParameterNames) + $jsonParameterContent = $jsonParameterContent.Replace($toReplaceValue, ('<{0}>' -f $expectedValue)) + } + } else { + # Case 2: Uses ARM-JSON parameter file + $jsonParameterContent = $rawContent.TrimEnd() } - # If we have at least one required and one other parameter we want to add a comment - if ($requiredParameterNames.Count -ge 1 -and $orderedJSONParameters.Keys.Count -ge 2) { + # --------------------- # + # Add Bicep example # + # --------------------- # + if ($addBicep) { + + # [1/4] Get all parameters from the parameter object + $JSONParametersHashTable = (ConvertFrom-Json $jsonParameterContent -AsHashtable -Depth 99).parameters + + # [2/4] Handle the special case of Key Vault secret references (that have a 'reference' instead of a 'value' property) + # [2.1] Find all references and split them into managable objects + $keyVaultReferences = $JSONParametersHashTable.Keys | Where-Object { $JSONParametersHashTable[$_].Keys -contains 'reference' } + + if ($keyVaultReferences.Count -gt 0) { + $keyVaultReferenceData = @() + foreach ($reference in $keyVaultReferences) { + $resourceIdElem = $JSONParametersHashTable[$reference].reference.keyVault.id -split '/' + $keyVaultReferenceData += @{ + subscriptionId = $resourceIdElem[2] + resourceGroupName = $resourceIdElem[4] + vaultName = $resourceIdElem[-1] + secretName = $JSONParametersHashTable[$reference].reference.secretName + parameterName = $reference + } + } + } - $jsonExampleArray = $jsonExample -split '\n' + # [2.2] Remove any duplicates from the referenced key vaults and build 'existing' Key Vault references in Bicep format from them. + # Also, add a link to the corresponding Key Vault 'resource' to each identified Key Vault secret reference + $extendedKeyVaultReferences = @() + $counter = 0 + foreach ($reference in ($keyVaultReferenceData | Sort-Object -Property 'vaultName' -Unique)) { + $counter++ + $extendedKeyVaultReferences += @( + "resource kv$counter 'Microsoft.KeyVault/vaults@2019-09-01' existing = {", + (" name: '{0}'" -f $reference.vaultName), + (" scope: resourceGroup('{0}','{1}')" -f $reference.subscriptionId, $reference.resourceGroupName), + '}', + '' + ) - # Check where the 'last' required parameter is located in the example (and what its indent is) - $parameterToSplitAt = $requiredParameterNames[-1] - $parameterStartIndex = ($jsonExampleArray | Select-String '.*"parameters": \{.*' | ForEach-Object { $_.LineNumber - 1 })[0] - $requiredParameterIndent = ([regex]::Match($jsonExampleArray[($parameterStartIndex + 1)], '^(\s+).*')).Captures.Groups[1].Value.Length + # Add attribute for later correct reference + $keyVaultReferenceData | Where-Object { $_.vaultName -eq $reference.vaultName } | ForEach-Object { + $_['vaultResourceReference'] = "kv$counter" + } + } - # Add a comment where the required parameters start - $jsonExampleArray = $jsonExampleArray[0..$parameterStartIndex] + ('{0}// Required parameters' -f (' ' * $requiredParameterIndent)) + $jsonExampleArray[(($parameterStartIndex + 1) .. ($jsonExampleArray.Count))] + # [3/5] Remove the 'value' property from each parameter + # If we're handling a classic ARM-JSON parameter file that includes replacing all 'references' with the link to one of the 'existing' Key Vault resources + if ((ConvertFrom-Json $rawContent -Depth 99).'$schema' -like '*deploymentParameters*') { + # If handling a classic parameter file + $JSONParameters = (ConvertFrom-Json $rawContent -Depth 99 -AsHashtable -NoEnumerate).parameters + $JSONParametersWithoutValue = @{} + foreach ($parameterName in $JSONParameters.psbase.Keys) { + if ($JSONParameters[$parameterName].Keys -eq 'value') { + $JSONParametersWithoutValue[$parameterName] = $JSONParameters[$parameterName]['value'] + } else { + # replace key vault references + $matchingTuple = $keyVaultReferenceData | Where-Object { $_.parameterName -eq $parameterName } + $JSONParametersWithoutValue[$parameterName] = "{0}.getSecret('{1}')" -f $matchingTuple.vaultResourceReference, $matchingTuple.secretName + } + } + } else { + # If handling a test deployment file + $JSONParametersWithoutValue = @{} + foreach ($parameter in $JSONParametersHashTable.Keys) { + $JSONParametersWithoutValue[$parameter] = $JSONParametersHashTable.$parameter.value + } + } - # Find the location if the last required parameter - $requiredParameterStartIndex = ($jsonExampleArray | Select-String "^[\s]{$requiredParameterIndent}`"$parameterToSplitAt`": \{.*" | ForEach-Object { $_.LineNumber - 1 })[0] + # [4/5] Convert the JSON parameters to a Bicep parameters block + $conversionInputObject = @{ + JSONParameters = $JSONParametersWithoutValue + RequiredParametersList = $null -ne $RequiredParametersList ? $RequiredParametersList : @() + } + $bicepExample = ConvertTo-FormattedBicep @conversionInputObject + + # [5/5] Create the final content block: That means + # - the 'existing' Key Vault resources + # - a 'module' header that mimics a module deployment + # - all parameters in Bicep format + $SectionContent += @( + '', + '

' + '' + 'via Bicep module' + '' + '```bicep', + $extendedKeyVaultReferences, + "module $resourceType './$resourceTypeIdentifier/deploy.bicep' = {" + " name: '`${uniqueString(deployment().name)}-$resourceType'" + ' params: {' + $bicepExample.TrimEnd(), + ' }' + '}' + '```', + '', + '
' + '

' + ) + } - # If we have more than only required parameters, let's add a corresponding comment - if ($orderedJSONParameters.Keys.Count -gt $requiredParameterNames.Count ) { - # Search in rest of array for the next closing bracket with the same indent - and then add the search index (1) & initial index (1) count back in - $requiredParameterEndIndex = ($jsonExampleArray[($requiredParameterStartIndex + 1)..($jsonExampleArray.Count)] | Select-String "^[\s]{$requiredParameterIndent}\}" | ForEach-Object { $_.LineNumber - 1 })[0] + 1 + $requiredParameterStartIndex + # -------------------- # + # Add JSON example # + # -------------------- # + if ($addJson) { - # Add a comment where the non-required parameters start - $jsonExampleArray = $jsonExampleArray[0..$requiredParameterEndIndex] + ('{0}// Non-required parameters' -f (' ' * $requiredParameterIndent)) + $jsonExampleArray[(($requiredParameterEndIndex + 1) .. ($jsonExampleArray.Count))] + # [1/2] Get all parameters from the parameter object and order them recursively + $orderingInputObject = @{ + ParametersJSON = (($jsonParameterContent | ConvertFrom-Json).parameters | ConvertTo-Json -Depth 99) + RequiredParametersList = $null -ne $RequiredParametersList ? $RequiredParametersList : @() } - - $jsonExample = $jsonExampleArray | Out-String + $orderedJSONExample = Build-OrderedJSONObject @orderingInputObject + + # [2/2] Create the final content block + $SectionContent += @( + '', + '

', + '', + 'via JSON Parameter file', + '', + '```json', + $orderedJSONExample.TrimEnd(), + '```', + '', + '
' + '

' + ) } - - $SectionContent += @( - '', - '

', - '', - 'via JSON Parameter file', - '', - '```json', - $jsonExample.TrimEnd(), - '```', - '', - '
' - '

' - ) } $SectionContent += @( @@ -603,7 +1070,9 @@ function Set-DeploymentExamplesSection { $pathIndex++ } - # Build result + ###################### + ## Built result ## + ###################### if ($SectionContent) { if ($PSCmdlet.ShouldProcess('Original file with new template references content', 'Merge')) { return Merge-FileWithNewContent -oldContent $ReadMeFileContent -newContent $SectionContent -SectionStartIdentifier $SectionStartIdentifier @@ -777,6 +1246,15 @@ function Set-ModuleReadMe { $fullResourcePath = (Split-Path $TemplateFilePath -Parent).Replace('\', '/').split('/modules/')[1] + $root = (Get-Item $PSScriptRoot).Parent.Parent.FullName + $projectSettingsPath = Join-Path $root 'settings.json' + if (Test-Path $projectSettingsPath) { + $projectSettings = Get-Content $projectSettingsPath | ConvertFrom-Json -AsHashtable + } else { + Write-Warning "No settings file found in path [$projectSettingsPath]" + $projectSettings = @{} + } + # Check readme if (-not (Test-Path $ReadMeFilePath) -or ([String]::IsNullOrEmpty((Get-Content $ReadMeFilePath -Raw)))) { # Create new readme file @@ -862,6 +1340,7 @@ function Set-ModuleReadMe { ReadMeFileContent = $readMeFileContent TemplateFilePath = $TemplateFilePath TemplateFileContent = $templateFileContent + ProjectSettings = $projectSettings } $readMeFileContent = Set-DeploymentExamplesSection @inputObject } From 57e9fb1a126c35693a07a4635f6203b1963c39e0 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Fri, 15 Jul 2022 18:16:38 +0200 Subject: [PATCH 02/14] Updated Set-ModuleReadMe with Bicep support & regenerated docs --- .../Microsoft.ApiManagement/service/readme.md | 14 +- .../configurationStores/readme.md | 16 +-- .../automationAccounts/readme.md | 31 +---- modules/Microsoft.Cache/redis/readme.md | 126 ++++++++++-------- .../virtualMachines/readme.md | 2 +- modules/Microsoft.EventGrid/topics/readme.md | 2 +- .../Microsoft.EventHub/namespaces/readme.md | 2 +- .../webPubSub/readme.md | 4 +- modules/Microsoft.Sql/servers/readme.md | 9 +- .../hostingEnvironments/readme.md | 14 +- 10 files changed, 85 insertions(+), 135 deletions(-) diff --git a/modules/Microsoft.ApiManagement/service/readme.md b/modules/Microsoft.ApiManagement/service/readme.md index f77ca283df..59707bf500 100644 --- a/modules/Microsoft.ApiManagement/service/readme.md +++ b/modules/Microsoft.ApiManagement/service/readme.md @@ -352,12 +352,7 @@ module service './Microsoft.ApiManagement/service/deploy.bicep' = { secret: true } ] - policies: [ - { - format: 'xml' - value: ' ' - } - ] + policies: ' ' portalSettings: [ { name: 'signin' @@ -670,12 +665,7 @@ module service './Microsoft.ApiManagement/service/deploy.bicep' = { publisherName: '<>-az-amorg-x-001' // Non-required parameters lock: 'CanNotDelete' - policies: [ - { - format: 'xml' - value: ' ' - } - ] + policies: ' ' portalSettings: [ { name: 'signin' diff --git a/modules/Microsoft.AppConfiguration/configurationStores/readme.md b/modules/Microsoft.AppConfiguration/configurationStores/readme.md index 7651115a55..4c988e81e4 100644 --- a/modules/Microsoft.AppConfiguration/configurationStores/readme.md +++ b/modules/Microsoft.AppConfiguration/configurationStores/readme.md @@ -340,21 +340,7 @@ module configurationStores './Microsoft.AppConfiguration/configurationStores/dep diagnosticWorkspaceId: '/subscriptions/<>/resourcegroups/validation-rg/providers/microsoft.operationalinsights/workspaces/adp-<>-az-law-x-001' disableLocalAuth: false enablePurgeProtection: false - keyValues: [ - { - contentType: 'contentType' - name: 'keyName' - roleAssignments: [ - { - principalIds: [ - '<>' - ] - roleDefinitionIdOrName: 'Reader' - } - ] - value: 'valueName' - } - ] + keyValues: 'valueName' lock: 'CanNotDelete' privateEndpoints: [ { diff --git a/modules/Microsoft.Automation/automationAccounts/readme.md b/modules/Microsoft.Automation/automationAccounts/readme.md index ec1b90645a..54ad2786f9 100644 --- a/modules/Microsoft.Automation/automationAccounts/readme.md +++ b/modules/Microsoft.Automation/automationAccounts/readme.md @@ -586,32 +586,11 @@ module automationAccounts './Microsoft.Automation/automationAccounts/deploy.bice '/subscriptions/<>/resourcegroups/validation-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/adp-<>-az-msi-x-001': {} } variables: [ - { - description: 'TestStringDescription' - name: 'TestString' - value: '\'TestString\'' - } - { - description: 'TestIntegerDescription' - name: 'TestInteger' - value: '500' - } - { - description: 'TestBooleanDescription' - name: 'TestBoolean' - value: 'false' - } - { - description: 'TestDateTimeDescription' - isEncrypted: false - name: 'TestDateTime' - value: '\'\\/Date(1637934042656)\\/\'' - } - { - description: 'TestEncryptedDescription' - name: 'TestEncryptedVariable' - value: '\'TestEncryptedValue\'' - } + '\'\\/Date(1637934042656)\\/\'' + '\'TestEncryptedValue\'' + '\'TestString\'' + '500' + 'false' ] } } diff --git a/modules/Microsoft.Cache/redis/readme.md b/modules/Microsoft.Cache/redis/readme.md index 36d14ec9ed..dc9e7ee61a 100644 --- a/modules/Microsoft.Cache/redis/readme.md +++ b/modules/Microsoft.Cache/redis/readme.md @@ -318,7 +318,27 @@ privateEndpoints: [ ## Deployment examples -

Example 1

+The following module usage examples are retrieved from the content of the files hosted in the module's `.test` folder. + >**Note**: The name of each example is based on the name of the file from which it is taken. + >**Note**: Each example lists all the required parameters first, followed by the rest - each in alphabetical order. + +

Example 1: Min

+ +
+ +via Bicep module + +```bicep +module redis './Microsoft.Cache/redis/deploy.bicep' = { + name: '${uniqueString(deployment().name)}-redis' + params: { + name: '<>-az-redis-min-001' + } +} +``` + +
+

@@ -337,6 +357,9 @@ privateEndpoints: [ ```
+

+ +

Example 2: Parameters

@@ -346,7 +369,35 @@ privateEndpoints: [ module redis './Microsoft.Cache/redis/deploy.bicep' = { name: '${uniqueString(deployment().name)}-redis' params: { - name: '<>-az-redis-min-001' + // Required parameters + name: '<>-az-redis-full-001' + // Non-required parameters + capacity: 2 + diagnosticLogCategoriesToEnable: [ + 'ApplicationGatewayAccessLog' + 'ApplicationGatewayFirewallLog' + ] + diagnosticMetricsToEnable: [ + 'AllMetrics' + ] + diagnosticSettingsName: 'redisdiagnostics' + enableNonSslPort: true + lock: 'CanNotDelete' + minimumTlsVersion: '1.2' + privateEndpoints: [ + { + service: 'redisCache' + subnetResourceId: '/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.Network/virtualNetworks/adp-<>-az-vnet-x-001/subnets/<>-az-subnet-x-005-privateEndpoints' + } + ] + publicNetworkAccess: 'Enabled' + redisVersion: '6' + shardCount: 1 + skuName: 'Premium' + systemAssignedIdentity: true + tags: { + resourceType: 'Redis Cache' + } } } ``` @@ -354,8 +405,6 @@ module redis './Microsoft.Cache/redis/deploy.bicep' = {

-

Example 2

-
via JSON Parameter file @@ -365,9 +414,11 @@ module redis './Microsoft.Cache/redis/deploy.bicep' = { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { + // Required parameters "name": { "value": "<>-az-redis-full-001" }, + // Non-required parameters "capacity": { "value": 2 }, @@ -382,6 +433,9 @@ module redis './Microsoft.Cache/redis/deploy.bicep' = { "AllMetrics" ] }, + "diagnosticSettingsName": { + "value": "redisdiagnostics" + }, "enableNonSslPort": { "value": true }, @@ -391,8 +445,13 @@ module redis './Microsoft.Cache/redis/deploy.bicep' = { "minimumTlsVersion": { "value": "1.2" }, - "diagnosticSettingsName": { - "value": "redisdiagnostics" + "privateEndpoints": { + "value": [ + { + "service": "redisCache", + "subnetResourceId": "/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.Network/virtualNetworks/adp-<>-az-vnet-x-001/subnets/<>-az-subnet-x-005-privateEndpoints" + } + ] }, "publicNetworkAccess": { "value": "Enabled" @@ -400,72 +459,23 @@ module redis './Microsoft.Cache/redis/deploy.bicep' = { "redisVersion": { "value": "6" }, + "shardCount": { + "value": 1 + }, "skuName": { "value": "Premium" }, "systemAssignedIdentity": { "value": true }, - "shardCount": { - "value": 1 - }, "tags": { "value": { "resourceType": "Redis Cache" } - }, - "privateEndpoints": { - "value": [ - { - "subnetResourceId": "/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.Network/virtualNetworks/adp-<>-az-vnet-x-001/subnets/<>-az-subnet-x-005-privateEndpoints", - "service": "redisCache" - } - ] } } } ``` -
- -
- -via Bicep module - -```bicep -module redis './Microsoft.Cache/redis/deploy.bicep' = { - name: '${uniqueString(deployment().name)}-redis' - params: { - name: '<>-az-redis-full-001' - capacity: 2 - diagnosticLogCategoriesToEnable: [ - 'ApplicationGatewayAccessLog' - 'ApplicationGatewayFirewallLog' - ] - diagnosticMetricsToEnable: [ - 'AllMetrics' - ] - enableNonSslPort: true - lock: 'CanNotDelete' - minimumTlsVersion: '1.2' - diagnosticSettingsName: 'redisdiagnostics' - publicNetworkAccess: 'Enabled' - redisVersion: '6' - skuName: 'Premium' - systemAssignedIdentity: true - shardCount: 1 - tags: { - resourceType: 'Redis Cache' - } - privateEndpoints: [ - { - subnetResourceId: '/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.Network/virtualNetworks/adp-<>-az-vnet-x-001/subnets/<>-az-subnet-x-005-privateEndpoints' - service: 'redisCache' - } - ] - } -} -``` -

diff --git a/modules/Microsoft.Compute/virtualMachines/readme.md b/modules/Microsoft.Compute/virtualMachines/readme.md index 2577539230..cb958231cd 100644 --- a/modules/Microsoft.Compute/virtualMachines/readme.md +++ b/modules/Microsoft.Compute/virtualMachines/readme.md @@ -22,7 +22,7 @@ This module deploys one Virtual Machine with one or multiple nics and optionally | `Microsoft.Insights/diagnosticSettings` | [2021-05-01-preview](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Insights/2021-05-01-preview/diagnosticSettings) | | `Microsoft.Network/networkInterfaces` | [2021-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Network/2021-05-01/networkInterfaces) | | `Microsoft.Network/publicIPAddresses` | [2021-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Network/2021-05-01/publicIPAddresses) | -| `Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems` | [2021-06-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.RecoveryServices/2021-06-01/vaults/backupFabrics/protectionContainers/protectedItems) | +| `Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems` | [2022-02-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.RecoveryServices/2022-02-01/vaults/backupFabrics/protectionContainers/protectedItems) | ## Parameters diff --git a/modules/Microsoft.EventGrid/topics/readme.md b/modules/Microsoft.EventGrid/topics/readme.md index 2d51148e49..0a26c1b4c2 100644 --- a/modules/Microsoft.EventGrid/topics/readme.md +++ b/modules/Microsoft.EventGrid/topics/readme.md @@ -17,7 +17,7 @@ This module deploys an event grid topic. | `Microsoft.Authorization/roleAssignments` | [2020-10-01-preview](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2020-10-01-preview/roleAssignments) | | `Microsoft.EventGrid/topics` | [2020-06-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventGrid/2020-06-01/topics) | | `Microsoft.Insights/diagnosticSettings` | [2021-05-01-preview](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Insights/2021-05-01-preview/diagnosticSettings) | -| `Microsoft.Network/privateEndpoints` | [2021-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Network/2021-05-01/privateEndpoints) | +| `Microsoft.Network/privateEndpoints` | [2021-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Network/privateEndpoints) | | `Microsoft.Network/privateEndpoints/privateDnsZoneGroups` | [2021-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Network/2021-05-01/privateEndpoints/privateDnsZoneGroups) | ## Parameters diff --git a/modules/Microsoft.EventHub/namespaces/readme.md b/modules/Microsoft.EventHub/namespaces/readme.md index d29240ade3..b8ab6251ae 100644 --- a/modules/Microsoft.EventHub/namespaces/readme.md +++ b/modules/Microsoft.EventHub/namespaces/readme.md @@ -17,7 +17,7 @@ This module deploys an event hub namespace. | `Microsoft.Authorization/roleAssignments` | [2020-10-01-preview](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2020-10-01-preview/roleAssignments) | | `Microsoft.EventHub/namespaces` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces) | | `Microsoft.EventHub/namespaces/authorizationRules` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces/authorizationRules) | -| `Microsoft.EventHub/namespaces/disasterRecoveryConfigs` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces/disasterRecoveryConfigs) | +| `Microsoft.EventHub/namespaces/disasterRecoveryConfigs` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/namespaces/disasterRecoveryConfigs) | | `Microsoft.EventHub/namespaces/eventhubs` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces/eventhubs) | | `Microsoft.EventHub/namespaces/eventhubs/authorizationRules` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces/eventhubs/authorizationRules) | | `Microsoft.EventHub/namespaces/eventhubs/consumergroups` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces/eventhubs/consumergroups) | diff --git a/modules/Microsoft.SignalRService/webPubSub/readme.md b/modules/Microsoft.SignalRService/webPubSub/readme.md index 1b7bf13e22..3342c6ba66 100644 --- a/modules/Microsoft.SignalRService/webPubSub/readme.md +++ b/modules/Microsoft.SignalRService/webPubSub/readme.md @@ -396,7 +396,7 @@ module webPubSub './Microsoft.SignalRService/webPubSub/deploy.bicep' = { params: { // Required parameters name: '<>-az-pubsub-x-001' - // Additional parameters + // Non-required parameters capacity: 2 clientCertEnabled: false disableAadAuth: false @@ -465,7 +465,7 @@ module webPubSub './Microsoft.SignalRService/webPubSub/deploy.bicep' = { "name": { "value": "<>-az-pubsub-x-001" }, - // Additional parameters + // Non-required parameters "capacity": { "value": 2 }, diff --git a/modules/Microsoft.Sql/servers/readme.md b/modules/Microsoft.Sql/servers/readme.md index a97a414d5d..78623cef94 100644 --- a/modules/Microsoft.Sql/servers/readme.md +++ b/modules/Microsoft.Sql/servers/readme.md @@ -397,7 +397,7 @@ module servers './Microsoft.Sql/servers/deploy.bicep' = { // Required parameters name: '<>-az-sqlsrv-x-001' // Non-required parameters - administratorLogin: kv1.getSecret('administratorLogin') + administratorLogin: 'adminUserName' administratorLoginPassword: kv1.getSecret('administratorLoginPassword') databases: [ { @@ -483,12 +483,7 @@ module servers './Microsoft.Sql/servers/deploy.bicep' = { }, // Non-required parameters "administratorLogin": { - "reference": { - "keyVault": { - "id": "/subscriptions/<>/resourceGroups/<>/providers/Microsoft.KeyVault/vaults/adp-<>-az-kv-x-001" - }, - "secretName": "administratorLogin" - } + "value": "adminUserName" }, "administratorLoginPassword": { "reference": { diff --git a/modules/Microsoft.Web/hostingEnvironments/readme.md b/modules/Microsoft.Web/hostingEnvironments/readme.md index 7b9c7aa52b..7150c84990 100644 --- a/modules/Microsoft.Web/hostingEnvironments/readme.md +++ b/modules/Microsoft.Web/hostingEnvironments/readme.md @@ -217,12 +217,7 @@ module hostingEnvironments './Microsoft.Web/hostingEnvironments/deploy.bicep' = name: '<>-az-appse-asev2-001' subnetResourceId: '/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.Network/virtualNetworks/adp-<>-az-vnet-x-001/subnets/<>-az-subnet-x-008' // Non-required parameters - clusterSettings: [ - { - name: 'DisableTls1.0' - value: '1' - } - ] + clusterSettings: '1' diagnosticEventHubAuthorizationRuleId: '/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.EventHub/namespaces/adp-<>-az-evhns-x-001/AuthorizationRules/RootManageSharedAccessKey' diagnosticEventHubName: 'adp-<>-az-evh-x-001' diagnosticLogsRetentionInDays: 7 @@ -326,12 +321,7 @@ module hostingEnvironments './Microsoft.Web/hostingEnvironments/deploy.bicep' = name: '<>-az-appse-asev3-001' subnetResourceId: '/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.Network/virtualNetworks/adp-<>-az-vnet-x-001/subnets/<>-az-subnet-x-006' // Non-required parameters - clusterSettings: [ - { - name: 'DisableTls1.0' - value: '1' - } - ] + clusterSettings: '1' diagnosticEventHubAuthorizationRuleId: '/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.EventHub/namespaces/adp-<>-az-evhns-x-001/AuthorizationRules/RootManageSharedAccessKey' diagnosticEventHubName: 'adp-<>-az-evh-x-001' diagnosticLogsRetentionInDays: 7 From 6b06637e8a75049b411f5ff9ed2dc432f0e1e8ad Mon Sep 17 00:00:00 2001 From: MrMCake Date: Fri, 15 Jul 2022 18:21:29 +0200 Subject: [PATCH 03/14] Updated comments --- utilities/tools/Set-ModuleReadMe.ps1 | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/utilities/tools/Set-ModuleReadMe.ps1 b/utilities/tools/Set-ModuleReadMe.ps1 index c2f108806b..bdb5faa154 100644 --- a/utilities/tools/Set-ModuleReadMe.ps1 +++ b/utilities/tools/Set-ModuleReadMe.ps1 @@ -354,13 +354,13 @@ function Add-BicepParameterTypeComment { $parameterToSplitAt = $RequiredParametersList[-1] $requiredParameterIndent = ([regex]::Match($BicepParamsArray[0], '^(\s+).*')).Captures.Groups[1].Value.Length - # [1/4] Add a comment where the required parameters start + # [2/4] Add a comment where the required parameters start $BicepParamsArray = @('{0}// Required parameters' -f (' ' * $requiredParameterIndent)) + $BicepParamsArray[(0 .. ($BicepParamsArray.Count))] - # [1/4] Find the location if the last required parameter + # [3/4] Find the location if the last required parameter $requiredParameterStartIndex = ($BicepParamsArray | Select-String ('^[\s]{0}{1}:.+' -f "{$requiredParameterIndent}", $parameterToSplitAt) | ForEach-Object { $_.LineNumber - 1 })[0] - # [1/4] If we have more than only required parameters, let's add a corresponding comment + # [4/4] If we have more than only required parameters, let's add a corresponding comment if ($AllParametersList.Count -gt $RequiredParametersList.Count) { $nextLineIndent = ([regex]::Match($BicepParamsArray[$requiredParameterStartIndex + 1], '^(\s+).*')).Captures.Groups[1].Value.Length if ($nextLineIndent -gt $requiredParameterIndent) { @@ -568,22 +568,22 @@ function ConvertTo-FormattedJSONParameterObject { $topLevelParams = $bicepParamBlockArray | Where-Object { $_ -match "^\s{$topLevelParamIndent}[0-9a-zA-Z]+:.*" } | ForEach-Object { ($_ -split ':')[0].Trim() } # [2/4] Add JSON-specific syntax to the Bicep param block to enable us to treat is as such - # [1.1] Syntax: Outer brackets + # [2.1] Syntax: Outer brackets $paramInJsonFormat = @( '{', $BicepParamBlock '}' ) | Out-String - # [1.2] Syntax: All single-quotes are double-quotes + # [2.2] Syntax: All single-quotes are double-quotes $paramInJsonFormat = $paramInJsonFormat -replace "'", '"' - # [1.3] Syntax: Everything left of a ':' should be wrapped in quotes (as a parameter name is always a string) + # [2.3] Syntax: Everything left of a ':' should be wrapped in quotes (as a parameter name is always a string) $paramInJsonFormat = $paramInJsonFormat -replace '([0-9a-zA-Z]+):', '"$1":' - # [1.4] Split the object to format line-by-line (& also remove any empty lines) + # [2.4] Split the object to format line-by-line (& also remove any empty lines) $paramInJSONFormatArray = $paramInJsonFormat -split '\n' | Where-Object { $_ } - # [1.5] Syntax: Replace Bicep resource ID references + # [2.5] Syntax: Replace Bicep resource ID references for ($index = 0; $index -lt $paramInJSONFormatArray.Count; $index++) { if ($paramInJSONFormatArray[$index] -like '*:*' -and ($paramInJSONFormatArray[$index] -split ':')[1].Trim() -notmatch '".+"' -and $paramInJSONFormatArray[$index] -like '*.*') { # In case of a reference like : "virtualWanId": resourceGroupResources.outputs.virtualWWANResourceId @@ -595,7 +595,7 @@ function ConvertTo-FormattedJSONParameterObject { } } - # [1.6] Syntax: Add comma everywhere unless: + # [2.6] Syntax: Add comma everywhere unless: # - the current line has an opening 'object: {' or 'array: [' character # - the line after the current line has a closing 'object: {' or 'array: [' character # - it's the last closing bracket @@ -606,7 +606,7 @@ function ConvertTo-FormattedJSONParameterObject { $paramInJSONFormatArray[$index] = '{0},' -f $paramInJSONFormatArray[$index].Trim() } - # [1.7] Format the final JSON string to an object to enable processing + # [2.7] Format the final JSON string to an object to enable processing $paramInJsonFormatObject = $paramInJSONFormatArray | Out-String | ConvertFrom-Json -AsHashtable -Depth 99 # [3/4] Inject top-level 'value`' properties @@ -876,14 +876,14 @@ function Set-DeploymentExamplesSection { # -------------------- # if ($addJson) { - # [2/3] Get all parameters from the parameter object and order them recursively + # [1/2] Get all parameters from the parameter object and order them recursively $orderingInputObject = @{ ParametersJSON = $paramsInJSONFormat | ConvertTo-Json -Depth 99 RequiredParametersList = $RequiredParametersList } $orderedJSONExample = Build-OrderedJSONObject @orderingInputObject - # [3/3] Create the final content block + # [2/2] Create the final content block $SectionContent += @( '', '

' @@ -938,10 +938,10 @@ function Set-DeploymentExamplesSection { # --------------------- # if ($addBicep) { - # [1/4] Get all parameters from the parameter object + # [1/5] Get all parameters from the parameter object $JSONParametersHashTable = (ConvertFrom-Json $jsonParameterContent -AsHashtable -Depth 99).parameters - # [2/4] Handle the special case of Key Vault secret references (that have a 'reference' instead of a 'value' property) + # [2/5] Handle the special case of Key Vault secret references (that have a 'reference' instead of a 'value' property) # [2.1] Find all references and split them into managable objects $keyVaultReferences = $JSONParametersHashTable.Keys | Where-Object { $JSONParametersHashTable[$_].Keys -contains 'reference' } From 30963faf12747fe983d6170ef82fbc3233cf84cf Mon Sep 17 00:00:00 2001 From: MrMCake Date: Fri, 15 Jul 2022 18:37:43 +0200 Subject: [PATCH 04/14] Updated robustness --- utilities/tools/Set-ModuleReadMe.ps1 | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/utilities/tools/Set-ModuleReadMe.ps1 b/utilities/tools/Set-ModuleReadMe.ps1 index bdb5faa154..f7d627b55a 100644 --- a/utilities/tools/Set-ModuleReadMe.ps1 +++ b/utilities/tools/Set-ModuleReadMe.ps1 @@ -661,11 +661,12 @@ function ConvertTo-FormattedBicep { # Remove 'value' parameter property, if any (e.g. when dealing with a classic parameter file) $JSONParametersWithoutValue = @{} - foreach ($parameter in $JSONParameters.psbase.Keys) { - if ($JSONParameters[$parameter].Keys -eq 'value') { - $JSONParametersWithoutValue[$parameter] = $JSONParameters.$parameter.value + foreach ($parameterName in $JSONParameters.psbase.Keys) { + $keysOnLevel = $JSONParameters[$parameterName].Keys + if ($keysOnLevel.count -eq 1 -and $keysOnLevel -eq 'value') { + $JSONParametersWithoutValue[$parameterName] = $JSONParameters[$parameterName].value } else { - $JSONParametersWithoutValue[$parameter] = $JSONParameters.$parameter + $JSONParametersWithoutValue[$parameterName] = $JSONParameters[$parameterName] } } @@ -986,7 +987,8 @@ function Set-DeploymentExamplesSection { $JSONParameters = (ConvertFrom-Json $rawContent -Depth 99 -AsHashtable -NoEnumerate).parameters $JSONParametersWithoutValue = @{} foreach ($parameterName in $JSONParameters.psbase.Keys) { - if ($JSONParameters[$parameterName].Keys -eq 'value') { + $keysOnLevel = $JSONParameters[$parameter].Keys + if ($keysOnLevel.count -eq 1 -and $keysOnLevel -eq 'value') { $JSONParametersWithoutValue[$parameterName] = $JSONParameters[$parameterName]['value'] } else { # replace key vault references From c79f4db454e1415896309a75f7ebb838cc07f46f Mon Sep 17 00:00:00 2001 From: MrMCake Date: Fri, 15 Jul 2022 18:40:15 +0200 Subject: [PATCH 05/14] Updated robustness --- utilities/tools/Set-ModuleReadMe.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilities/tools/Set-ModuleReadMe.ps1 b/utilities/tools/Set-ModuleReadMe.ps1 index f7d627b55a..c1fe108be1 100644 --- a/utilities/tools/Set-ModuleReadMe.ps1 +++ b/utilities/tools/Set-ModuleReadMe.ps1 @@ -987,7 +987,7 @@ function Set-DeploymentExamplesSection { $JSONParameters = (ConvertFrom-Json $rawContent -Depth 99 -AsHashtable -NoEnumerate).parameters $JSONParametersWithoutValue = @{} foreach ($parameterName in $JSONParameters.psbase.Keys) { - $keysOnLevel = $JSONParameters[$parameter].Keys + $keysOnLevel = $JSONParameters[$parameterName].Keys if ($keysOnLevel.count -eq 1 -and $keysOnLevel -eq 'value') { $JSONParametersWithoutValue[$parameterName] = $JSONParameters[$parameterName]['value'] } else { From 3c1a59fcae95a1ce4f9dbec06e3b8009bf8d28f5 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Fri, 15 Jul 2022 18:47:41 +0200 Subject: [PATCH 06/14] Updated docs --- .../Microsoft.ApiManagement/service/readme.md | 14 +++++++-- .../configurationStores/readme.md | 16 +++++++++- .../automationAccounts/readme.md | 31 ++++++++++++++++--- .../Microsoft.EventHub/namespaces/readme.md | 2 +- .../hostingEnvironments/readme.md | 14 +++++++-- 5 files changed, 66 insertions(+), 11 deletions(-) diff --git a/modules/Microsoft.ApiManagement/service/readme.md b/modules/Microsoft.ApiManagement/service/readme.md index 59707bf500..f77ca283df 100644 --- a/modules/Microsoft.ApiManagement/service/readme.md +++ b/modules/Microsoft.ApiManagement/service/readme.md @@ -352,7 +352,12 @@ module service './Microsoft.ApiManagement/service/deploy.bicep' = { secret: true } ] - policies: ' ' + policies: [ + { + format: 'xml' + value: ' ' + } + ] portalSettings: [ { name: 'signin' @@ -665,7 +670,12 @@ module service './Microsoft.ApiManagement/service/deploy.bicep' = { publisherName: '<>-az-amorg-x-001' // Non-required parameters lock: 'CanNotDelete' - policies: ' ' + policies: [ + { + format: 'xml' + value: ' ' + } + ] portalSettings: [ { name: 'signin' diff --git a/modules/Microsoft.AppConfiguration/configurationStores/readme.md b/modules/Microsoft.AppConfiguration/configurationStores/readme.md index 4c988e81e4..7651115a55 100644 --- a/modules/Microsoft.AppConfiguration/configurationStores/readme.md +++ b/modules/Microsoft.AppConfiguration/configurationStores/readme.md @@ -340,7 +340,21 @@ module configurationStores './Microsoft.AppConfiguration/configurationStores/dep diagnosticWorkspaceId: '/subscriptions/<>/resourcegroups/validation-rg/providers/microsoft.operationalinsights/workspaces/adp-<>-az-law-x-001' disableLocalAuth: false enablePurgeProtection: false - keyValues: 'valueName' + keyValues: [ + { + contentType: 'contentType' + name: 'keyName' + roleAssignments: [ + { + principalIds: [ + '<>' + ] + roleDefinitionIdOrName: 'Reader' + } + ] + value: 'valueName' + } + ] lock: 'CanNotDelete' privateEndpoints: [ { diff --git a/modules/Microsoft.Automation/automationAccounts/readme.md b/modules/Microsoft.Automation/automationAccounts/readme.md index 54ad2786f9..ec1b90645a 100644 --- a/modules/Microsoft.Automation/automationAccounts/readme.md +++ b/modules/Microsoft.Automation/automationAccounts/readme.md @@ -586,11 +586,32 @@ module automationAccounts './Microsoft.Automation/automationAccounts/deploy.bice '/subscriptions/<>/resourcegroups/validation-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/adp-<>-az-msi-x-001': {} } variables: [ - '\'\\/Date(1637934042656)\\/\'' - '\'TestEncryptedValue\'' - '\'TestString\'' - '500' - 'false' + { + description: 'TestStringDescription' + name: 'TestString' + value: '\'TestString\'' + } + { + description: 'TestIntegerDescription' + name: 'TestInteger' + value: '500' + } + { + description: 'TestBooleanDescription' + name: 'TestBoolean' + value: 'false' + } + { + description: 'TestDateTimeDescription' + isEncrypted: false + name: 'TestDateTime' + value: '\'\\/Date(1637934042656)\\/\'' + } + { + description: 'TestEncryptedDescription' + name: 'TestEncryptedVariable' + value: '\'TestEncryptedValue\'' + } ] } } diff --git a/modules/Microsoft.EventHub/namespaces/readme.md b/modules/Microsoft.EventHub/namespaces/readme.md index b8ab6251ae..d29240ade3 100644 --- a/modules/Microsoft.EventHub/namespaces/readme.md +++ b/modules/Microsoft.EventHub/namespaces/readme.md @@ -17,7 +17,7 @@ This module deploys an event hub namespace. | `Microsoft.Authorization/roleAssignments` | [2020-10-01-preview](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2020-10-01-preview/roleAssignments) | | `Microsoft.EventHub/namespaces` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces) | | `Microsoft.EventHub/namespaces/authorizationRules` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces/authorizationRules) | -| `Microsoft.EventHub/namespaces/disasterRecoveryConfigs` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/namespaces/disasterRecoveryConfigs) | +| `Microsoft.EventHub/namespaces/disasterRecoveryConfigs` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces/disasterRecoveryConfigs) | | `Microsoft.EventHub/namespaces/eventhubs` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces/eventhubs) | | `Microsoft.EventHub/namespaces/eventhubs/authorizationRules` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces/eventhubs/authorizationRules) | | `Microsoft.EventHub/namespaces/eventhubs/consumergroups` | [2021-11-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventHub/2021-11-01/namespaces/eventhubs/consumergroups) | diff --git a/modules/Microsoft.Web/hostingEnvironments/readme.md b/modules/Microsoft.Web/hostingEnvironments/readme.md index 7150c84990..7b9c7aa52b 100644 --- a/modules/Microsoft.Web/hostingEnvironments/readme.md +++ b/modules/Microsoft.Web/hostingEnvironments/readme.md @@ -217,7 +217,12 @@ module hostingEnvironments './Microsoft.Web/hostingEnvironments/deploy.bicep' = name: '<>-az-appse-asev2-001' subnetResourceId: '/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.Network/virtualNetworks/adp-<>-az-vnet-x-001/subnets/<>-az-subnet-x-008' // Non-required parameters - clusterSettings: '1' + clusterSettings: [ + { + name: 'DisableTls1.0' + value: '1' + } + ] diagnosticEventHubAuthorizationRuleId: '/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.EventHub/namespaces/adp-<>-az-evhns-x-001/AuthorizationRules/RootManageSharedAccessKey' diagnosticEventHubName: 'adp-<>-az-evh-x-001' diagnosticLogsRetentionInDays: 7 @@ -321,7 +326,12 @@ module hostingEnvironments './Microsoft.Web/hostingEnvironments/deploy.bicep' = name: '<>-az-appse-asev3-001' subnetResourceId: '/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.Network/virtualNetworks/adp-<>-az-vnet-x-001/subnets/<>-az-subnet-x-006' // Non-required parameters - clusterSettings: '1' + clusterSettings: [ + { + name: 'DisableTls1.0' + value: '1' + } + ] diagnosticEventHubAuthorizationRuleId: '/subscriptions/<>/resourceGroups/validation-rg/providers/Microsoft.EventHub/namespaces/adp-<>-az-evhns-x-001/AuthorizationRules/RootManageSharedAccessKey' diagnosticEventHubName: 'adp-<>-az-evh-x-001' diagnosticLogsRetentionInDays: 7 From 994ad66517c87aba7467ebbc782a1f4dd81a015c Mon Sep 17 00:00:00 2001 From: MrMCake Date: Fri, 15 Jul 2022 18:49:09 +0200 Subject: [PATCH 07/14] Another update --- modules/Microsoft.EventGrid/topics/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/Microsoft.EventGrid/topics/readme.md b/modules/Microsoft.EventGrid/topics/readme.md index 0a26c1b4c2..2d51148e49 100644 --- a/modules/Microsoft.EventGrid/topics/readme.md +++ b/modules/Microsoft.EventGrid/topics/readme.md @@ -17,7 +17,7 @@ This module deploys an event grid topic. | `Microsoft.Authorization/roleAssignments` | [2020-10-01-preview](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2020-10-01-preview/roleAssignments) | | `Microsoft.EventGrid/topics` | [2020-06-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.EventGrid/2020-06-01/topics) | | `Microsoft.Insights/diagnosticSettings` | [2021-05-01-preview](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Insights/2021-05-01-preview/diagnosticSettings) | -| `Microsoft.Network/privateEndpoints` | [2021-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Network/privateEndpoints) | +| `Microsoft.Network/privateEndpoints` | [2021-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Network/2021-05-01/privateEndpoints) | | `Microsoft.Network/privateEndpoints/privateDnsZoneGroups` | [2021-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Network/2021-05-01/privateEndpoints/privateDnsZoneGroups) | ## Parameters From ece3e5f89804080d9ec24be92bd7bcd11af14312 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Fri, 15 Jul 2022 18:57:28 +0200 Subject: [PATCH 08/14] Changed casing --- utilities/tools/Set-ModuleReadMe.ps1 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/utilities/tools/Set-ModuleReadMe.ps1 b/utilities/tools/Set-ModuleReadMe.ps1 index c1fe108be1..1e8236c3e1 100644 --- a/utilities/tools/Set-ModuleReadMe.ps1 +++ b/utilities/tools/Set-ModuleReadMe.ps1 @@ -31,7 +31,7 @@ function Set-ResourceTypesSection { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ResourceTypesToExclude', Justification = 'Variable used inside Where-Object block.')] param ( [Parameter(Mandatory)] - [Hashtable] $TemplateFileContent, + [hashtable] $TemplateFileContent, [Parameter(Mandatory)] [object[]] $ReadMeFileContent, @@ -118,7 +118,7 @@ function Set-ParametersSection { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] - [Hashtable] $TemplateFileContent, + [hashtable] $TemplateFileContent, [Parameter(Mandatory)] [object[]] $ReadMeFileContent, @@ -187,8 +187,8 @@ function Set-ParametersSection { } # Add external single quotes to all default values of type string except for those using functions - $defaultValue = ($parameter.defaultValue -is [array]) ? ('[{0}]' -f ($parameter.defaultValue -join ', ')) : (($parameter.defaultValue -is [Hashtable]) ? '{object}' : (($parameter.defaultValue -is [string]) -and ($parameter.defaultValue -notmatch '\[\w+\(.*\).*\]') ? '''' + $parameter.defaultValue + '''' : $parameter.defaultValue)) - $allowedValue = ($parameter.allowedValues -is [array]) ? ('[{0}]' -f ($parameter.allowedValues -join ', ')) : (($parameter.allowedValues -is [Hashtable]) ? '{object}' : $parameter.allowedValues) + $defaultValue = ($parameter.defaultValue -is [array]) ? ('[{0}]' -f ($parameter.defaultValue -join ', ')) : (($parameter.defaultValue -is [hashtable]) ? '{object}' : (($parameter.defaultValue -is [string]) -and ($parameter.defaultValue -notmatch '\[\w+\(.*\).*\]') ? '''' + $parameter.defaultValue + '''' : $parameter.defaultValue)) + $allowedValue = ($parameter.allowedValues -is [array]) ? ('[{0}]' -f ($parameter.allowedValues -join ', ')) : (($parameter.allowedValues -is [hashtable]) ? '{object}' : $parameter.allowedValues) $description = $parameter.metadata.description.Replace("`r`n", '

').Replace("`n", '

') # Update parameter table content based on parameter category @@ -260,7 +260,7 @@ function Set-OutputsSection { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] - [Hashtable] $TemplateFileContent, + [hashtable] $TemplateFileContent, [Parameter(Mandatory)] [object[]] $ReadMeFileContent, @@ -652,7 +652,7 @@ function ConvertTo-FormattedBicep { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [Hashtable] $JSONParameters, + [hashtable] $JSONParameters, [Parameter(Mandatory = $false)] [AllowEmptyCollection()] @@ -746,7 +746,7 @@ function Set-DeploymentExamplesSection { [string] $TemplateFilePath, [Parameter(Mandatory)] - [Hashtable] $TemplateFileContent, + [hashtable] $TemplateFileContent, [Parameter(Mandatory = $true)] [object[]] $ReadMeFileContent, @@ -758,7 +758,7 @@ function Set-DeploymentExamplesSection { [bool] $addBicep = $true, [Parameter(Mandatory = $false)] - [Hashtable] $ProjectSettings = @{}, + [hashtable] $ProjectSettings = @{}, [Parameter(Mandatory = $false)] [string] $SectionStartIdentifier = '## Deployment examples' @@ -1203,7 +1203,7 @@ function Set-ModuleReadMe { [string] $TemplateFilePath, [Parameter(Mandatory = $false)] - [Hashtable] $TemplateFileContent, + [hashtable] $TemplateFileContent, [Parameter(Mandatory = $false)] [string] $ReadMeFilePath = (Join-Path (Split-Path $TemplateFilePath -Parent) 'readme.md'), From 42572e7aa5bcb36b309474662f4f48c5785b9e8a Mon Sep 17 00:00:00 2001 From: MrMCake Date: Fri, 15 Jul 2022 19:25:47 +0200 Subject: [PATCH 09/14] Added error handling --- utilities/tools/Set-ModuleReadMe.ps1 | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/utilities/tools/Set-ModuleReadMe.ps1 b/utilities/tools/Set-ModuleReadMe.ps1 index 1e8236c3e1..fc2460afcd 100644 --- a/utilities/tools/Set-ModuleReadMe.ps1 +++ b/utilities/tools/Set-ModuleReadMe.ps1 @@ -1235,10 +1235,15 @@ function Set-ModuleReadMe { $TemplateFilePath = Resolve-Path -Path $TemplateFilePath -ErrorAction Stop if (-not $TemplateFileContent) { - if ((Split-Path -Path $TemplateFilePath -Extension) -eq '.bicep') { - $templateFileContent = az bicep build --file $TemplateFilePath --stdout --no-restore | ConvertFrom-Json -AsHashtable + + if (-not (Test-Path $TemplateFilePath -PathType 'Leaf')) { + throw "[$TemplateFilePath] is no valid file path." } else { - $templateFileContent = ConvertFrom-Json (Get-Content $TemplateFilePath -Encoding 'utf8' -Raw) -ErrorAction Stop -AsHashtable + if ((Split-Path -Path $TemplateFilePath -Extension) -eq '.bicep') { + $templateFileContent = az bicep build --file $TemplateFilePath --stdout --no-restore | ConvertFrom-Json -AsHashtable + } else { + $templateFileContent = ConvertFrom-Json (Get-Content $TemplateFilePath -Encoding 'utf8' -Raw) -ErrorAction Stop -AsHashtable + } } } From 629061b4e8ed90b0e26cd58efb2ef6d9f2bf1294 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Fri, 15 Jul 2022 19:26:10 +0200 Subject: [PATCH 10/14] Added error handling --- modules/Microsoft.Sql/servers/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/Microsoft.Sql/servers/readme.md b/modules/Microsoft.Sql/servers/readme.md index 78623cef94..98e4332416 100644 --- a/modules/Microsoft.Sql/servers/readme.md +++ b/modules/Microsoft.Sql/servers/readme.md @@ -329,7 +329,7 @@ The following module usage examples are retrieved from the content of the files

via Bicep module ```bicep -module servers './Microsoft.Sql/servers/deploy.bicep' = { +module servers './Microsoft.sql/servers/deploy.bicep' = { name: '${uniqueString(deployment().name)}-servers' params: { // Required parameters @@ -391,7 +391,7 @@ resource kv1 'Microsoft.KeyVault/vaults@2019-09-01' existing = { scope: resourceGroup('<>','<>') } -module servers './Microsoft.Sql/servers/deploy.bicep' = { +module servers './Microsoft.sql/servers/deploy.bicep' = { name: '${uniqueString(deployment().name)}-servers' params: { // Required parameters From 841bef2f9910dc76a9967a5df4da3ea8fd830065 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Mon, 18 Jul 2022 17:18:09 +0200 Subject: [PATCH 11/14] Updated docs --- modules/Microsoft.Sql/servers/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/Microsoft.Sql/servers/readme.md b/modules/Microsoft.Sql/servers/readme.md index 98a7658ab7..180febabc6 100644 --- a/modules/Microsoft.Sql/servers/readme.md +++ b/modules/Microsoft.Sql/servers/readme.md @@ -329,7 +329,7 @@ The following module usage examples are retrieved from the content of the files via Bicep module ```bicep -module servers './Microsoft.sql/servers/deploy.bicep' = { +module servers './Microsoft.Sql/servers/deploy.bicep' = { name: '${uniqueString(deployment().name)}-servers' params: { // Required parameters @@ -391,7 +391,7 @@ resource kv1 'Microsoft.KeyVault/vaults@2019-09-01' existing = { scope: resourceGroup('<>','<>') } -module servers './Microsoft.sql/servers/deploy.bicep' = { +module servers './Microsoft.Sql/servers/deploy.bicep' = { name: '${uniqueString(deployment().name)}-servers' params: { // Required parameters From 51a3b102479bd85e26197737ea4792824f0c7b2f Mon Sep 17 00:00:00 2001 From: MrMCake Date: Wed, 20 Jul 2022 21:44:42 +0200 Subject: [PATCH 12/14] Update to latest --- utilities/tools/Set-ModuleReadMe.ps1 | 892 +++++++++++++-------------- 1 file changed, 446 insertions(+), 446 deletions(-) diff --git a/utilities/tools/Set-ModuleReadMe.ps1 b/utilities/tools/Set-ModuleReadMe.ps1 index dc50153ea2..219c0319c4 100644 --- a/utilities/tools/Set-ModuleReadMe.ps1 +++ b/utilities/tools/Set-ModuleReadMe.ps1 @@ -305,589 +305,589 @@ function Set-OutputsSection { <# .SYNOPSIS -Generate 'Deployment examples' for the ReadMe out of the parameter files currently used to test the template +Add comments to indicate required & non-required parameters to the given Bicep example .DESCRIPTION -Generate 'Deployment examples' for the ReadMe out of the parameter files currently used to test the template - -.PARAMETER TemplateFilePath -Mandatory. The path to the template file - -.PARAMETER TemplateFileContent -Mandatory. The template file content object to crawl data from +Add comments to indicate required & non-required parameters to the given Bicep example. +'Required' is only added if the example has at least one required parameter +'Non-Required' is only added if the example has at least one required parameter and at least one non-required parameter -.PARAMETER ReadMeFileContent -Mandatory. The readme file content array to update +.PARAMETER BicepParams +Mandatory. The Bicep parameter block to add the comments to (i.e., should contain everything in between the brackets of a 'params: {...} block) -.PARAMETER SectionStartIdentifier -Optional. The identifier of the 'outputs' section. Defaults to '## Deployment examples' +.PARAMETER AllParametersList +Mandatory. A list of all top-level (i.e. non-nested) parameter names -.PARAMETER addJson -Optional. A switch to control whether or not to add a ARM-JSON-Parameter file example. Defaults to true. - -.PARAMETER addBicep -Optional. A switch to control whether or not to add a Bicep deployment example. Defaults to true. +.PARAMETER RequiredParametersList +Mandatory. A list of all required top-level (i.e. non-nested) parameter names .EXAMPLE -Set-DeploymentExamplesSection -TemplateFilePath 'C:/deploy.bicep' -TemplateFileContent @{ resource = @{}; ... } -ReadMeFileContent @('# Title', '', '## Section 1', ...) +Add-BicepParameterTypeComment -AllParametersList @('name', 'lock') -RequiredParametersList @('name') -BicepParams "name: 'carml'\nlock: 'CanNotDelete'" -Update the given readme file's 'Deployment Examples' section based on the given template file content +Add type comments to given bicep params string, using one required parameter 'name'. Would return: + +' + // Required parameters + name: 'carml' + // Non-required parameters + lock: 'CanNotDelete' +' #> -function Set-DeploymentExamplesSection { +function Add-BicepParameterTypeComment { - [CmdletBinding(SupportsShouldProcess)] + [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [string] $TemplateFilePath, - - [Parameter(Mandatory)] - [hashtable] $TemplateFileContent, - - [Parameter(Mandatory = $true)] - [object[]] $ReadMeFileContent, + [AllowEmptyString()] + [string] $BicepParams, [Parameter(Mandatory = $false)] - [bool] $addJson = $true, + [AllowEmptyCollection()] + [string[]] $AllParametersList = @(), [Parameter(Mandatory = $false)] - [bool] $addBicep = $true, - - [Parameter(Mandatory = $false)] - [string] $SectionStartIdentifier = '## Deployment examples' + [AllowEmptyCollection()] + [string[]] $RequiredParametersList = @() ) - # Load used function(s) - . (Join-Path $PSScriptRoot 'helper' 'ConvertTo-OrderedHashtable.ps1') + if ($RequiredParametersList.Count -ge 1 -and $AllParametersList.Count -ge 2) { - # Process content - $SectionContent = [System.Collections.ArrayList]@( - 'The following module usage examples are retrieved from the content of the files hosted in the module''s `.test` folder.', - ' >**Note**: The name of each example is based on the name of the file from which it is taken.', - ' >**Note**: Each example lists all the required parameters first, followed by the rest - each in alphabetical order.', - '' - ) + $BicepParamsArray = $BicepParams -split '\n' - $moduleRoot = Split-Path $TemplateFilePath -Parent - $resourceTypeIdentifier = $moduleRoot.Replace('\', '/').Split('/modules/')[1].TrimStart('/') - $resourceType = $resourceTypeIdentifier.Split('/')[1] - $parameterFiles = Get-ChildItem (Join-Path $moduleRoot '.test') -Filter '*parameters.json' -Recurse + # [1/4] Check where the 'last' required parameter is located in the example (and what its indent is) + $parameterToSplitAt = $RequiredParametersList[-1] + $requiredParameterIndent = ([regex]::Match($BicepParamsArray[0], '^(\s+).*')).Captures.Groups[1].Value.Length - $requiredParameterNames = $TemplateFileContent.parameters.Keys | Where-Object { $TemplateFileContent.parameters[$_].Keys -notcontains 'defaultValue' } | Sort-Object + # [2/4] Add a comment where the required parameters start + $BicepParamsArray = @('{0}// Required parameters' -f (' ' * $requiredParameterIndent)) + $BicepParamsArray[(0 .. ($BicepParamsArray.Count))] - ############################ - ## Process test files ## - ############################ - $pathIndex = 1 - foreach ($testFilePath in $parameterFiles.FullName) { - $contentInJSONFormat = Get-Content -Path $testFilePath -Encoding 'utf8' | Out-String - - $exampleTitle = ((Split-Path $testFilePath -LeafBase) -replace '\.', ' ') -replace ' parameters', '' - $TextInfo = (Get-Culture).TextInfo - $exampleTitle = $TextInfo.ToTitleCase($exampleTitle) - $SectionContent += @( - '

Example {0}: {1}

' -f $pathIndex, $exampleTitle - ) + # [3/4] Find the location if the last required parameter + $requiredParameterStartIndex = ($BicepParamsArray | Select-String ('^[\s]{0}{1}:.+' -f "{$requiredParameterIndent}", $parameterToSplitAt) | ForEach-Object { $_.LineNumber - 1 })[0] - if ($addBicep) { - $JSONParametersHashTable = (ConvertFrom-Json $contentInJSONFormat -AsHashtable -Depth 99).parameters - - # Handle KeyVaut references - $keyVaultReferences = $JSONParametersHashTable.Keys | Where-Object { $JSONParametersHashTable[$_].Keys -contains 'reference' } - - if ($keyVaultReferences.Count -gt 0) { - $keyVaultReferenceData = @() - foreach ($reference in $keyVaultReferences) { - $resourceIdElem = $JSONParametersHashTable[$reference].reference.keyVault.id -split '/' - $keyVaultReferenceData += @{ - subscriptionId = $resourceIdElem[2] - resourceGroupName = $resourceIdElem[4] - vaultName = $resourceIdElem[-1] - secretName = $JSONParametersHashTable[$reference].reference.secretName - parameterName = $reference - } - } + # [4/4] If we have more than only required parameters, let's add a corresponding comment + if ($AllParametersList.Count -gt $RequiredParametersList.Count) { + $nextLineIndent = ([regex]::Match($BicepParamsArray[$requiredParameterStartIndex + 1], '^(\s+).*')).Captures.Groups[1].Value.Length + if ($nextLineIndent -gt $requiredParameterIndent) { + # Case Param is object/array: Search in rest of array for the next closing bracket with the same indent - and then add the search index (1) & initial index (1) count back in + $requiredParameterEndIndex = ($BicepParamsArray[($requiredParameterStartIndex + 1)..($BicepParamsArray.Count)] | Select-String "^[\s]{$requiredParameterIndent}\S+" | ForEach-Object { $_.LineNumber - 1 })[0] + 1 + $requiredParameterStartIndex + } else { + # Case Param is single line bool/string/int: Add an index (1) for the 'required' comment + $requiredParameterEndIndex = $requiredParameterStartIndex } - $extendedKeyVaultReferences = @() - $counter = 0 - foreach ($reference in ($keyVaultReferenceData | Sort-Object -Property 'vaultName' -Unique)) { - $counter++ - $extendedKeyVaultReferences += @( - "resource kv$counter 'Microsoft.KeyVault/vaults@2019-09-01' existing = {", - (" name: '{0}'" -f $reference.vaultName), - (" scope: resourceGroup('{0}','{1}')" -f $reference.subscriptionId, $reference.resourceGroupName), - '}', - '' - ) + # Add a comment where the non-required parameters start + $BicepParamsArray = $BicepParamsArray[0..$requiredParameterEndIndex] + ('{0}// Non-required parameters' -f (' ' * $requiredParameterIndent)) + $BicepParamsArray[(($requiredParameterEndIndex + 1) .. ($BicepParamsArray.Count))] + } - # Add attribute for later correct reference - $keyVaultReferenceData | Where-Object { $_.vaultName -eq $reference.vaultName } | ForEach-Object { - $_['vaultResourceReference'] = "kv$counter" - } - } + return ($BicepParamsArray | Out-String).TrimEnd() + } - # Handle VALUE references (i.e. remove them) - $JSONParameters = (ConvertFrom-Json $contentInJSONFormat -Depth 99 -AsHashtable).parameters - $JSONParametersWithoutValue = @{} - foreach ($parameterName in $JSONParameters.Keys) { - if ($JSONParameters[$parameterName].Keys -eq 'value') { - $JSONParametersWithoutValue[$parameterName] = $JSONParameters[$parameterName]['value'] - } else { - # replace key vault references - $matchingTuple = $keyVaultReferenceData | Where-Object { $_.parameterName -eq $parameterName } - $JSONParametersWithoutValue[$parameterName] = "{0}.getSecret('{1}')" -f $matchingTuple.vaultResourceReference, $matchingTuple.secretName - } - } + return $BicepParams +} - # Order parameters recursively - $JSONParametersWithoutValue = ConvertTo-OrderedHashtable -JSONInputObject ($JSONParametersWithoutValue | ConvertTo-Json -Depth 99) +<# +.SYNOPSIS +Sort the given JSON paramters into required & non-required parameters, each sorted alphabetically - # Sort 'required' parameters to the front - $orderedJSONParameters = [ordered]@{} - $orderedTopLevelParameterNames = $JSONParametersWithoutValue.psbase.Keys # We must use PS-Base to handle conflicts of HashTable properties & keys (e.g. for a key 'keys'). - # Add required parameters first - $orderedTopLevelParameterNames | Where-Object { $_ -in $requiredParameterNames } | ForEach-Object { $orderedJSONParameters[$_] = $JSONParametersWithoutValue[$_] } - # Add rest after - $orderedTopLevelParameterNames | Where-Object { $_ -notin $requiredParameterNames } | ForEach-Object { $orderedJSONParameters[$_] = $JSONParametersWithoutValue[$_] } +.DESCRIPTION +Sort the given JSON paramters into required & non-required parameters, each sorted alphabetically - if ($orderedJSONParameters.count -eq 0) { - # Handle empty dictionaries (in case the parmaeter file was empty) - $orderedJSONParameters = @{} - } +.PARAMETER ParametersJSON +Mandatory. The JSON parameters block to process (ideally already without 'value' property) - $templateParameterObject = $orderedJSONParameters | ConvertTo-Json -Depth 99 - if ($templateParameterObject -ne '{}') { - $contentInBicepFormat = $templateParameterObject -replace '"', "'" # Update any [xyz: "xyz"] to [xyz: 'xyz'] - $contentInBicepFormat = $contentInBicepFormat -replace ',', '' # Update any [xyz: xyz,] to [xyz: xyz] - $contentInBicepFormat = $contentInBicepFormat -replace "'(\w+)':", '$1:' # Update any ['xyz': xyz] to [xyz: xyz] - $contentInBicepFormat = $contentInBicepFormat -replace "'(.+.getSecret\('.+'\))'", '$1' # Update any [xyz: 'xyz.GetSecret()'] to [xyz: xyz.GetSecret()] +.PARAMETER RequiredParametersList +Mandatory. A list of all required top-level (i.e. non-nested) parameter names - $bicepParamsArray = $contentInBicepFormat -split '\n' - $bicepParamsArray = $bicepParamsArray[1..($bicepParamsArray.count - 2)] - } +.EXAMPLE +Get-OrderedParametersJSON -RequiredParametersList @('name') -ParametersJSON '{ "diagnosticLogsRetentionInDays": 7,"lock": "CanNotDelete","name": "carml" }' - # Format params with indent - $bicepExample = $bicepParamsArray | ForEach-Object { " $_" } +Order the given JSON object alphabetically. Would result into: - # Optional: Add comment where required & optional parameters start - # ---------------------------------------------------------------- - if ($requiredParameterNames -is [string]) { - $requiredParameterNames = @($requiredParameterNames) - } +@{ + name: 'carml' + diagnosticLogsRetentionInDays: 7 + lock: 'CanNotDelete' +} +#> +function Get-OrderedParametersJSON { - # If we have at least one required and one other parameter we want to add a comment - if ($requiredParameterNames.Count -ge 1 -and $orderedJSONParameters.Keys.Count -ge 2) { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $ParametersJSON, - $bicepExampleArray = $bicepExample -split '\n' + [Parameter(Mandatory = $false)] + [AllowEmptyCollection()] + [string[]] $RequiredParametersList = @() + ) - # Check where the 'last' required parameter is located in the example (and what its indent is) - $parameterToSplitAt = $requiredParameterNames[-1] - $requiredParameterIndent = ([regex]::Match($bicepExampleArray[0], '^(\s+).*')).Captures.Groups[1].Value.Length + # Load used function(s) + . (Join-Path $PSScriptRoot 'helper' 'ConvertTo-OrderedHashtable.ps1') - # Add a comment where the required parameters start - $bicepExampleArray = @('{0}// Required parameters' -f (' ' * $requiredParameterIndent)) + $bicepExampleArray[(0 .. ($bicepExampleArray.Count))] + # [1/3] Get all parameters from the parameter object and order them recursively + $orderedContentInJSONFormat = ConvertTo-OrderedHashtable -JSONInputObject $parametersJSON - # Find the location if the last required parameter - $requiredParameterStartIndex = ($bicepExampleArray | Select-String ('^[\s]{0}{1}:.+' -f "{$requiredParameterIndent}", $parameterToSplitAt) | ForEach-Object { $_.LineNumber - 1 })[0] + # [2/3] Sort 'required' parameters to the front + $orderedJSONParameters = [ordered]@{} + $orderedTopLevelParameterNames = $orderedContentInJSONFormat.psbase.Keys # We must use PS-Base to handle conflicts of HashTable properties & keys (e.g. for a key 'keys'). + # [2.1] Add required parameters first + $orderedTopLevelParameterNames | Where-Object { $_ -in $RequiredParametersList } | ForEach-Object { $orderedJSONParameters[$_] = $orderedContentInJSONFormat[$_] } + # [2.2] Add rest after + $orderedTopLevelParameterNames | Where-Object { $_ -notin $RequiredParametersList } | ForEach-Object { $orderedJSONParameters[$_] = $orderedContentInJSONFormat[$_] } - # If we have more than only required parameters, let's add a corresponding comment - if ($orderedJSONParameters.Keys.Count -gt $requiredParameterNames.Count) { - $nextLineIndent = ([regex]::Match($bicepExampleArray[$requiredParameterStartIndex + 1], '^(\s+).*')).Captures.Groups[1].Value.Length - if ($nextLineIndent -gt $requiredParameterIndent) { - # Case Param is object/array: Search in rest of array for the next closing bracket with the same indent - and then add the search index (1) & initial index (1) count back in - $requiredParameterEndIndex = ($bicepExampleArray[($requiredParameterStartIndex + 1)..($bicepExampleArray.Count)] | Select-String "^[\s]{$requiredParameterIndent}\S+" | ForEach-Object { $_.LineNumber - 1 })[0] + 1 + $requiredParameterStartIndex - } else { - # Case Param is single line bool/string/int: Add an index (1) for the 'required' comment - $requiredParameterEndIndex = $requiredParameterStartIndex - } + # [3/3] Handle empty dictionaries (in case the parmaeter file was empty) + if ($orderedJSONParameters.count -eq 0) { + $orderedJSONParameters = '' + } - # Add a comment where the non-required parameters start - $bicepExampleArray = $bicepExampleArray[0..$requiredParameterEndIndex] + ('{0}// Non-required parameters' -f (' ' * $requiredParameterIndent)) + $bicepExampleArray[(($requiredParameterEndIndex + 1) .. ($bicepExampleArray.Count))] - } + return $orderedJSONParameters +} - $bicepExample = $bicepExampleArray | Out-String - } +<# +.SYNOPSIS +Sort the given JSON parameters into a new JSON parameter object, all parameter sorted into required & non-required parameters, each sorted alphabetically - $SectionContent += @( - '', - '
' - '' - 'via Bicep module' - '' - '```bicep', - $extendedKeyVaultReferences, - "module $resourceType './$resourceTypeIdentifier/deploy.bicep' = {" - " name: '`${uniqueString(deployment().name)}-$resourceType'" - ' params: {' - $bicepExample.TrimEnd(), - ' }' - '}' - '```', - '', - '
' - '

' - ) - } +.DESCRIPTION +Sort the given JSON parameters into a new JSON parameter object, all parameter sorted into required & non-required parameters, each sorted alphabetically. +The location where required & non-required parameters start is highlighted with by a corresponding comment - if ($addJson) { - $orderedContentInJSONFormat = ConvertTo-OrderedHashtable -JSONInputObject (($contentInJSONFormat | ConvertFrom-Json).parameters | ConvertTo-Json -Depth 99) +.PARAMETER ParametersJSON +Mandatory. The parameter JSON object to process - # Sort 'required' parameters to the front - $orderedJSONParameters = [ordered]@{} - $orderedTopLevelParameterNames = $orderedContentInJSONFormat.psbase.Keys # We must use PS-Base to handle conflicts of HashTable properties & keys (e.g. for a key 'keys'). - # Add required parameters first - $orderedTopLevelParameterNames | Where-Object { $_ -in $requiredParameterNames } | ForEach-Object { $orderedJSONParameters[$_] = $orderedContentInJSONFormat[$_] } - # Add rest after - $orderedTopLevelParameterNames | Where-Object { $_ -notin $requiredParameterNames } | ForEach-Object { $orderedJSONParameters[$_] = $orderedContentInJSONFormat[$_] } +.PARAMETER RequiredParametersList +Mandatory. A list of all required top-level (i.e. non-nested) parameter names - if ($orderedJSONParameters.count -eq 0) { - # Handle empty dictionaries (in case the parmaeter file was empty) - $orderedJSONParameters = '' - } +.EXAMPLE +Build-OrderedJSONObject -RequiredParametersList @('name') -ParametersJSON '{ "lock": { "value": "CanNotDelete" }, "name": { "value": "carml" }, "diagnosticLogsRetentionInDays": { "value": 7 } }' + +Build a formatted Parameter-JSON object with one required parameter. Would result into: + +'{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Required parameters + "name": { + "value": "carml" + }, + // Non-required parameters + "diagnosticLogsRetentionInDays": { + "value": 7 + }, + "lock": { + "value": "CanNotDelete" + } + } +}' +#> +function Build-OrderedJSONObject { - $jsonExample = ([ordered]@{ - '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' - contentVersion = '1.0.0.0' - parameters = (-not [String]::IsNullOrEmpty($orderedJSONParameters)) ? $orderedJSONParameters : @{} - } | ConvertTo-Json -Depth 99) + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $ParametersJSON, - # Optional: Add comment where required & optional parameters start - # ---------------------------------------------------------------- - if ($requiredParameterNames -is [string]) { - $requiredParameterNames = @($requiredParameterNames) - } + [Parameter(Mandatory = $false)] + [AllowEmptyCollection()] + [string[]] $RequiredParametersList = @() + ) - # If we have at least one required and one other parameter we want to add a comment - if ($requiredParameterNames.Count -ge 1 -and $orderedJSONParameters.Keys.Count -ge 2) { + # [1/9] Sort parameter alphabetically + $orderedJSONParameters = Get-OrderedParametersJSON -ParametersJSON $ParametersJSON -RequiredParametersList $RequiredParametersList - $jsonExampleArray = $jsonExample -split '\n' + # [2/9] Build the ordered parameter file syntax back up + $jsonExample = ([ordered]@{ + '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' + contentVersion = '1.0.0.0' + parameters = (-not [String]::IsNullOrEmpty($orderedJSONParameters)) ? $orderedJSONParameters : @{} + } | ConvertTo-Json -Depth 99) - # Check where the 'last' required parameter is located in the example (and what its indent is) - $parameterToSplitAt = $requiredParameterNames[-1] - $parameterStartIndex = ($jsonExampleArray | Select-String '.*"parameters": \{.*' | ForEach-Object { $_.LineNumber - 1 })[0] - $requiredParameterIndent = ([regex]::Match($jsonExampleArray[($parameterStartIndex + 1)], '^(\s+).*')).Captures.Groups[1].Value.Length + # [3/8] If we have at least one required and one other parameter we want to add a comment + if ($RequiredParametersList.Count -ge 1 -and $OrderedJSONParameters.Keys.Count -ge 2) { - # Add a comment where the required parameters start - $jsonExampleArray = $jsonExampleArray[0..$parameterStartIndex] + ('{0}// Required parameters' -f (' ' * $requiredParameterIndent)) + $jsonExampleArray[(($parameterStartIndex + 1) .. ($jsonExampleArray.Count))] + $jsonExampleArray = $jsonExample -split '\n' - # Find the location if the last required parameter - $requiredParameterStartIndex = ($jsonExampleArray | Select-String "^[\s]{$requiredParameterIndent}`"$parameterToSplitAt`": \{.*" | ForEach-Object { $_.LineNumber - 1 })[0] + # [4/8] Check where the 'last' required parameter is located in the example (and what its indent is) + $parameterToSplitAt = $RequiredParametersList[-1] + $parameterStartIndex = ($jsonExampleArray | Select-String '.*"parameters": \{.*' | ForEach-Object { $_.LineNumber - 1 })[0] + $requiredParameterIndent = ([regex]::Match($jsonExampleArray[($parameterStartIndex + 1)], '^(\s+).*')).Captures.Groups[1].Value.Length - # If we have more than only required parameters, let's add a corresponding comment - if ($orderedJSONParameters.Keys.Count -gt $requiredParameterNames.Count ) { - # Search in rest of array for the next closing bracket with the same indent - and then add the search index (1) & initial index (1) count back in - $requiredParameterEndIndex = ($jsonExampleArray[($requiredParameterStartIndex + 1)..($jsonExampleArray.Count)] | Select-String "^[\s]{$requiredParameterIndent}\}" | ForEach-Object { $_.LineNumber - 1 })[0] + 1 + $requiredParameterStartIndex + # [5/8] Add a comment where the required parameters start + $jsonExampleArray = $jsonExampleArray[0..$parameterStartIndex] + ('{0}// Required parameters' -f (' ' * $requiredParameterIndent)) + $jsonExampleArray[(($parameterStartIndex + 1) .. ($jsonExampleArray.Count))] - # Add a comment where the non-required parameters start - $jsonExampleArray = $jsonExampleArray[0..$requiredParameterEndIndex] + ('{0}// Non-required parameters' -f (' ' * $requiredParameterIndent)) + $jsonExampleArray[(($requiredParameterEndIndex + 1) .. ($jsonExampleArray.Count))] - } + # [6/8] Find the location if the last required parameter + $requiredParameterStartIndex = ($jsonExampleArray | Select-String "^[\s]{$requiredParameterIndent}`"$parameterToSplitAt`": \{.*" | ForEach-Object { $_.LineNumber - 1 })[0] - $jsonExample = $jsonExampleArray | Out-String - } + # [7/8] If we have more than only required parameters, let's add a corresponding comment + if ($orderedJSONParameters.Keys.Count -gt $RequiredParametersList.Count ) { + # Search in rest of array for the next closing bracket with the same indent - and then add the search index (1) & initial index (1) count back in + $requiredParameterEndIndex = ($jsonExampleArray[($requiredParameterStartIndex + 1)..($jsonExampleArray.Count)] | Select-String "^[\s]{$requiredParameterIndent}\}" | ForEach-Object { $_.LineNumber - 1 })[0] + 1 + $requiredParameterStartIndex - $SectionContent += @( - '', - '

', - '', - 'via JSON Parameter file', - '', - '```json', - $jsonExample.TrimEnd(), - '```', - '', - '
' - '

' - ) + # Add a comment where the non-required parameters start + $jsonExampleArray = $jsonExampleArray[0..$requiredParameterEndIndex] + ('{0}// Non-required parameters' -f (' ' * $requiredParameterIndent)) + $jsonExampleArray[(($requiredParameterEndIndex + 1) .. ($jsonExampleArray.Count))] } - $SectionContent += @( - '' - ) + # [8/8] Convert the processed array back into a string + return $jsonExampleArray | Out-String + } - $pathIndex++ + return $jsonExample +} + +<# +.SYNOPSIS +Convert the given Bicep parameter block to JSON parameter block + +.DESCRIPTION +Convert the given Bicep parameter block to JSON parameter block + +.PARAMETER BicepParamBlock +Mandatory. The Bicep parameter block to process + +.EXAMPLE +ConvertTo-FormattedJSONParameterObject -BicepParamBlock "name: 'carml'\nlock: 'CanNotDelete'" + +Convert the Bicep string "name: 'carml'\nlock: 'CanNotDelete'" into a parameter JSON object. Would result into: + +@{ + lock = @{ + value = 'carml' + } + lock = @{ + value = 'CanNotDelete' } +} +#> +function ConvertTo-FormattedJSONParameterObject { - # Build result - if ($SectionContent) { - if ($PSCmdlet.ShouldProcess('Original file with new template references content', 'Merge')) { - return Merge-FileWithNewContent -oldContent $ReadMeFileContent -newContent $SectionContent -SectionStartIdentifier $SectionStartIdentifier + [CmdletBinding()] + param ( + [Parameter()] + [string] $BicepParamBlock + ) + + # [1/4] Detect top level params for later processing + $bicepParamBlockArray = $BicepParamBlock -split '\n' + $topLevelParamIndent = ([regex]::Match($bicepParamBlockArray[0], '^(\s+).*')).Captures.Groups[1].Value.Length + $topLevelParams = $bicepParamBlockArray | Where-Object { $_ -match "^\s{$topLevelParamIndent}[0-9a-zA-Z]+:.*" } | ForEach-Object { ($_ -split ':')[0].Trim() } + + # [2/4] Add JSON-specific syntax to the Bicep param block to enable us to treat is as such + # [2.1] Syntax: Outer brackets + $paramInJsonFormat = @( + '{', + $BicepParamBlock + '}' + ) | Out-String + + # [2.2] Syntax: All single-quotes are double-quotes + $paramInJsonFormat = $paramInJsonFormat -replace "'", '"' + # [2.3] Syntax: Everything left of a ':' should be wrapped in quotes (as a parameter name is always a string) + $paramInJsonFormat = $paramInJsonFormat -replace '([0-9a-zA-Z]+):', '"$1":' + + # [2.4] Split the object to format line-by-line (& also remove any empty lines) + $paramInJSONFormatArray = $paramInJsonFormat -split '\n' | Where-Object { $_ } + + # [2.5] Syntax: Replace Bicep resource ID references + for ($index = 0; $index -lt $paramInJSONFormatArray.Count; $index++) { + if ($paramInJSONFormatArray[$index] -like '*:*' -and ($paramInJSONFormatArray[$index] -split ':')[1].Trim() -notmatch '".+"' -and $paramInJSONFormatArray[$index] -like '*.*') { + # In case of a reference like : "virtualWanId": resourceGroupResources.outputs.virtualWWANResourceId + $paramInJSONFormatArray[$index] = '{0}: "<{1}>"' -f ($paramInJSONFormatArray[$index] -split ':')[0], ([regex]::Match(($paramInJSONFormatArray[$index] -split ':')[0], '"(.+)"')).Captures.Groups[1].Value + } + if ($paramInJSONFormatArray[$index] -notlike '*:*' -and $paramInJSONFormatArray[$index] -notlike '*"*"*' -and $paramInJSONFormatArray[$index] -like '*.*') { + # In case of a reference like : [ \n resourceGroupResources.outputs.managedIdentityPrincipalId \n ] + $paramInJSONFormatArray[$index] = '"<{0}>"' -f $paramInJSONFormatArray[$index].Split('.')[-1].Trim() + } + } + + # [2.6] Syntax: Add comma everywhere unless: + # - the current line has an opening 'object: {' or 'array: [' character + # - the line after the current line has a closing 'object: {' or 'array: [' character + # - it's the last closing bracket + for ($index = 0; $index -lt $paramInJSONFormatArray.Count; $index++) { + if (($paramInJSONFormatArray[$index] -match '[\{|\[]') -or (($index -lt $paramInJSONFormatArray.Count - 1) -and $paramInJSONFormatArray[$index + 1] -match '[\]|\}]') -or ($index -eq $paramInJSONFormatArray.Count - 1)) { + continue + } + $paramInJSONFormatArray[$index] = '{0},' -f $paramInJSONFormatArray[$index].Trim() + } + + # [2.7] Format the final JSON string to an object to enable processing + $paramInJsonFormatObject = $paramInJSONFormatArray | Out-String | ConvertFrom-Json -AsHashtable -Depth 99 + + # [3/4] Inject top-level 'value`' properties + $paramInJsonFormatObjectWithValue = @{} + foreach ($paramKey in $topLevelParams) { + $paramInJsonFormatObjectWithValue[$paramKey] = @{ + value = $paramInJsonFormatObject[$paramKey] } - } else { - return $ReadMeFileContent } + + # [4/4] Return result + return $paramInJsonFormatObjectWithValue } <# .SYNOPSIS -Generate a table of content section for the given readme file +Convert the given parameter JSON object into a formatted Bicep object (i.e., sorted & with required/non-required comments) .DESCRIPTION -Generate a table of content section for the given readme file +Convert the given parameter JSON object into a formatted Bicep object (i.e., sorted & with required/non-required comments) -.PARAMETER ReadMeFileContent -Mandatory. The readme file content array to update +.PARAMETER JSONParameters +Mandatory. The parameter JSON object to process. -.PARAMETER SectionStartIdentifier -Optional. The identifier of the 'navigation' section. Defaults to '## Navigation' +.PARAMETER RequiredParametersList +Mandatory. A list of all required top-level (i.e. non-nested) parameter names .EXAMPLE -Set-TableOfContent -ReadMeFileContent @('# Title', '', '## Section 1', ...) +ConvertTo-FormattedBicep -RequiredParametersList @('name') -JSONParameters @{ lock = @{ value = 'carml' }; lock = @{ value = 'CanNotDelete' } } -Update the given readme's '## Navigation' section to reflect the latest file structure +Convert the given JSONParameters object with one required parameter to a formatted Bicep object. Would result into: + +' + // Required parameters + name: 'carml' + // Non-required parameters + diagnosticLogsRetentionInDays: 7 + lock: 'CanNotDelete' +' #> -function Set-TableOfContent { +function ConvertTo-FormattedBicep { - [CmdletBinding(SupportsShouldProcess)] + [CmdletBinding()] param ( - [Parameter(Mandatory)] - [object[]] $ReadMeFileContent, + [Parameter(Mandatory = $true)] + [hashtable] $JSONParameters, [Parameter(Mandatory = $false)] - [string] $SectionStartIdentifier = '## Navigation' + [AllowEmptyCollection()] + [string[]] $RequiredParametersList = @() ) - $newSectionContent = [System.Collections.ArrayList]@() - - $contentPointer = 1 - while ($ReadMeFileContent[$contentPointer] -notlike '#*') { - $contentPointer++ + # Remove 'value' parameter property, if any (e.g. when dealing with a classic parameter file) + $JSONParametersWithoutValue = @{} + foreach ($parameterName in $JSONParameters.psbase.Keys) { + $keysOnLevel = $JSONParameters[$parameterName].Keys + if ($keysOnLevel.count -eq 1 -and $keysOnLevel -eq 'value') { + $JSONParametersWithoutValue[$parameterName] = $JSONParameters[$parameterName].value + } else { + $JSONParametersWithoutValue[$parameterName] = $JSONParameters[$parameterName] + } } - $headers = $ReadMeFileContent.Split('\n') | Where-Object { $_ -like '## *' } - - if ($headers -notcontains $SectionStartIdentifier) { - $beforeContent = $ReadMeFileContent[0 .. ($contentPointer - 1)] - $afterContent = $ReadMeFileContent[$contentPointer .. ($ReadMeFileContent.Count - 1)] - - $ReadMeFileContent = $beforeContent + @($SectionStartIdentifier, '') + $afterContent + # [1/4] Order parameters recursively + if ($JSONParametersWithoutValue.Keys.Count -gt 0) { + $orderedJSONParameters = Get-OrderedParametersJSON -ParametersJSON ($JSONParametersWithoutValue | ConvertTo-Json -Depth 99) -RequiredParametersList $RequiredParametersList + } else { + $orderedJSONParameters = @{} } - - $headers | Where-Object { $_ -ne $SectionStartIdentifier } | ForEach-Object { - $newSectionContent += '- [{0}](#{1})' -f $_.Replace('#', '').Trim(), $_.Replace('#', '').Trim().Replace(' ', '-').Replace('.', '') + # [2/4] Remove any JSON specific formatting + $templateParameterObject = $orderedJSONParameters | ConvertTo-Json -Depth 99 + if ($templateParameterObject -ne '{}') { + $contentInBicepFormat = $templateParameterObject -replace '"', "'" # Update any [xyz: "xyz"] to [xyz: 'xyz'] + $contentInBicepFormat = $contentInBicepFormat -replace ',', '' # Update any [xyz: xyz,] to [xyz: xyz] + $contentInBicepFormat = $contentInBicepFormat -replace "'(\w+)':", '$1:' # Update any ['xyz': xyz] to [xyz: xyz] + $contentInBicepFormat = $contentInBicepFormat -replace "'(.+.getSecret\('.+'\))'", '$1' # Update any [xyz: 'xyz.GetSecret()'] to [xyz: xyz.GetSecret()] + + $bicepParamsArray = $contentInBicepFormat -split '\n' + $bicepParamsArray = $bicepParamsArray[1..($bicepParamsArray.count - 2)] } - # Build result - if ($PSCmdlet.ShouldProcess('Original file with new parameters content', 'Merge')) { - $updatedFileContent = Merge-FileWithNewContent -oldContent $ReadMeFileContent -newContent $newSectionContent -SectionStartIdentifier $SectionStartIdentifier -contentType 'none' + # [3/4] Format params with indent + $BicepParams = ($bicepParamsArray | ForEach-Object { " $_" } | Out-String).TrimEnd() + + # [4/4] Add comment where required & optional parameters start + $splitInputObject = @{ + BicepParams = $BicepParams + RequiredParametersList = $RequiredParametersList + AllParametersList = $JSONParametersWithoutValue.Keys } + $commentedBicepParams = Add-BicepParameterTypeComment @splitInputObject - return $updatedFileContent + return $commentedBicepParams } -#endregion <# .SYNOPSIS -Update/add the readme that matches the given template file +Generate 'Deployment examples' for the ReadMe out of the parameter files currently used to test the template .DESCRIPTION -Update/add the readme that matches the given template file -Supports both ARM & bicep templates. +Generate 'Deployment examples' for the ReadMe out of the parameter files currently used to test the template .PARAMETER TemplateFilePath -Mandatory. The path to the template to update +Mandatory. The path to the template file .PARAMETER TemplateFileContent -Optional. The template file content to process. If not provided, the template file content will be read from the TemplateFilePath file. -Using this property is useful if you already compiled the bicep template before invoking this function and want to avoid re-compiling it. - -.PARAMETER ReadMeFilePath -Optional. The path to the readme to update. If not provided assumes a 'readme.md' file in the same folder as the template - -.PARAMETER SectionsToRefresh -Optional. The sections to update. By default it refreshes all that are supported. -Currently supports: 'Resource Types', 'Parameters', 'Outputs', 'Template references' - -.EXAMPLE -Set-ModuleReadMe -TemplateFilePath 'C:\deploy.bicep' - -Update the readme in path 'C:\readme.md' based on the bicep template in path 'C:\deploy.bicep' +Mandatory. The template file content object to crawl data from -.EXAMPLE -Set-ModuleReadMe -TemplateFilePath 'C:/Microsoft.Network/loadBalancers/deploy.bicep' -SectionsToRefresh @('Parameters', 'Outputs') +.PARAMETER TemplateFilePath +Mandatory. The path to the template file -Generate the Module ReadMe only for specific sections. Updates only the sections `Parameters` & `Outputs`. Other sections remain untouched. +.PARAMETER ReadMeFileContent +Mandatory. The readme file content array to update -.EXAMPLE -Set-ModuleReadMe -TemplateFilePath 'C:/Microsoft.Network/loadBalancers/deploy.bicep' -TemplateFileContent @{...} +.PARAMETER SectionStartIdentifier +Optional. The identifier of the 'outputs' section. Defaults to '## Deployment examples' -(Re)Generate the readme file for template 'loadBalancer' based on the content provided in the TemplateFileContent parameter +.PARAMETER addJson +Optional. A switch to control whether or not to add a ARM-JSON-Parameter file example. Defaults to true. -.EXAMPLE -Set-ModuleReadMe -TemplateFilePath 'C:/Microsoft.Network/loadBalancers/deploy.bicep' -ReadMeFilePath 'C:/differentFolder' +.PARAMETER addBicep +Optional. A switch to control whether or not to add a Bicep deployment example. Defaults to true. -Generate the Module ReadMe files into a specific folder path +.PARAMETER ProjectSettings +Optional. Projects settings to draw information from. For example the `namePrefix`. .EXAMPLE -$templatePaths = (Get-ChildItem 'C:/Microsoft.Network' -Filter 'deploy.bicep' -Recurse).FullName -$templatePaths | ForEach-Object -Parallel { . '/utilities/tools/Set-ModuleReadMe.ps1' ; Set-ModuleReadMe -TemplateFilePath $_ } - -Generate the Module ReadMe for any template in a folder path +Set-DeploymentExamplesSection -TemplateFileContent @{ resource = @{}; ... } -TemplateFilePath 'C:/deploy.bicep' -ReadMeFileContent @('# Title', '', '## Section 1', ...) -.NOTES -The script autopopulates the Parameter Usage section of the ReadMe with the matching content in path './moduleReadMeSource'. -The content is added in case the given template has a parameter that matches the suffix of one of the files in that path. -To account for more parameter, just add another markdown file with the naming pattern 'resourceUsage-' +Update the given readme file's 'Deployment Examples' section based on the given template file content #> -function Set-ModuleReadMe { +function Set-DeploymentExamplesSection { - [CmdletBinding(SupportsShouldProcess = $true)] + [CmdletBinding(SupportsShouldProcess)] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [string] $TemplateFilePath, + [Parameter(Mandatory)] + [hashtable] $TemplateFileContent, + + [Parameter(Mandatory = $true)] + [object[]] $ReadMeFileContent, + [Parameter(Mandatory = $false)] - [Hashtable] $TemplateFileContent, + [bool] $addJson = $true, [Parameter(Mandatory = $false)] - [string] $ReadMeFilePath = (Join-Path (Split-Path $TemplateFilePath -Parent) 'readme.md'), + [bool] $addBicep = $true, [Parameter(Mandatory = $false)] - [ValidateSet( - 'Resource Types', - 'Parameters', - 'Outputs', - 'Template references', - 'Navigation', - 'Deployment examples' - )] - [string[]] $SectionsToRefresh = @( - 'Resource Types', - 'Parameters', - 'Outputs', - 'Template references', - 'Navigation', - 'Deployment examples' - ) + [hashtable] $ProjectSettings = @{}, + + [Parameter(Mandatory = $false)] + [string] $SectionStartIdentifier = '## Deployment examples' ) - # Load external functions - . (Join-Path $PSScriptRoot 'helper/Merge-FileWithNewContent.ps1') - . (Join-Path (Split-Path $PSScriptRoot -Parent) 'pipelines' 'sharedScripts' 'Get-NestedResourceList.ps1') + # Process content + $SectionContent = [System.Collections.ArrayList]@( + 'The following module usage examples are retrieved from the content of the files hosted in the module''s `.test` folder.', + ' >**Note**: The name of each example is based on the name of the file from which it is taken.', + ' >**Note**: Each example lists all the required parameters first, followed by the rest - each in alphabetical order.', + '' + ) - # Check template & make full path - $TemplateFilePath = Resolve-Path -Path $TemplateFilePath -ErrorAction Stop + $moduleRoot = Split-Path $TemplateFilePath -Parent + $resourceTypeIdentifier = $moduleRoot.Replace('\', '/').Split('/modules/')[1].TrimStart('/') + $resourceType = $resourceTypeIdentifier.Split('/')[1] + $testFilePaths = (Get-ChildItem (Join-Path -Path $moduleRoot -ChildPath '.test') -File).FullName | Where-Object { $_ -match '.+\.[bicep|json]' } - if (-not $TemplateFileContent) { - if ((Split-Path -Path $TemplateFilePath -Extension) -eq '.bicep') { - $templateFileContent = az bicep build --file $TemplateFilePath --stdout | ConvertFrom-Json -AsHashtable - } else { - $templateFileContent = ConvertFrom-Json (Get-Content $TemplateFilePath -Encoding 'utf8' -Raw) -ErrorAction Stop -AsHashtable - } - } + $RequiredParametersList = $TemplateFileContent.parameters.Keys | Where-Object { $TemplateFileContent.parameters[$_].Keys -notcontains 'defaultValue' } | Sort-Object - if (-not $templateFileContent) { - throw "Failed to compile [$TemplateFilePath]" - } + ############################ + ## Process test files ## + ############################ + $pathIndex = 1 + foreach ($testFilePath in $testFilePaths) { - $fullResourcePath = (Split-Path $TemplateFilePath -Parent).Replace('\', '/').split('/modules/')[1] + # Read content + $rawContentArray = Get-Content -Path $testFilePath + $rawContent = Get-Content -Path $testFilePath -Encoding 'utf8' | Out-String - # Check readme - if (-not (Test-Path $ReadMeFilePath) -or ([String]::IsNullOrEmpty((Get-Content $ReadMeFilePath -Raw)))) { - # Create new readme file + # Format example header + $exampleTitle = ((Split-Path $testFilePath -LeafBase) -replace '\.', ' ') -replace ' parameters', '' + $TextInfo = (Get-Culture).TextInfo + $exampleTitle = $TextInfo.ToTitleCase($exampleTitle) + $SectionContent += @( + '

Example {0}: {1}

' -f $pathIndex, $exampleTitle + ) - # Build resource name - $serviceIdentifiers = $fullResourcePath.Replace('Microsoft.', '').Replace('/.', '/').Split('/') - $serviceIdentifiers = $serviceIdentifiers | ForEach-Object { $_.substring(0, 1).toupper() + $_.substring(1) } - $serviceIdentifiers = $serviceIdentifiers | ForEach-Object { $_ -creplace '(?<=\w)([A-Z])', '$1' } - $assumedResourceName = $serviceIdentifiers -join ' ' + ## ----------------------------------- ## + ## Handle by type (Bicep vs. JSON) ## + ## ----------------------------------- ## + if ((Split-Path $testFilePath -Extension) -eq '.bicep') { - $initialContent = @( - "# $assumedResourceName ``[$fullResourcePath]``", - '', - "This module deploys $assumedResourceName." - '// TODO: Replace Resource and fill in description', - '' - '## Resource Types', - '', - '## Parameters', - '', - '### Parameter Usage: ``' - '' - '// TODO: Fill in Parameter usage' - '', - '## Outputs' - ) - # New-Item $path $ReadMeFilePath -ItemType 'File' -Force -Value $initialContent - $readMeFileContent = $initialContent - } else { - $readMeFileContent = Get-Content -Path $ReadMeFilePath -Encoding 'utf8' - } + # ------------------------- # + # Prepare Bicep to JSON # + # ------------------------- # - # Update title - if ($TemplateFilePath.Replace('\', '/') -like '*/modules/*') { + # [1/6] Search for the relevant parameter start & end index + $bicepTestStartIndex = $rawContentArray.IndexOf("module testDeployment '../deploy.bicep' = {") - if ($readMeFileContent[0] -notlike "*``[$fullResourcePath]``") { - # Cut outdated - $readMeFileContent[0] = $readMeFileContent[0].Split('`[')[0] + $bicepTestEndIndex = $bicepTestStartIndex + do { + $bicepTestEndIndex++ + } while ($rawContentArray[$bicepTestEndIndex] -ne '}') - # Add latest - $readMeFileContent[0] = '{0} `[{1}]`' -f $readMeFileContent[0], $fullResourcePath - } - # Remove excess whitespace - $readMeFileContent[0] = $readMeFileContent[0] -replace '\s+', ' ' - } + $rawBicepExample = $rawContentArray[$bicepTestStartIndex..$bicepTestEndIndex] - if ($SectionsToRefresh -contains 'Resource Types') { - # Handle [Resource Types] section - # =============================== - $inputObject = @{ - ReadMeFileContent = $readMeFileContent - TemplateFileContent = $templateFileContent - } - $readMeFileContent = Set-ResourceTypesSection @inputObject - } + # [2/6] Replace placeholders + $namePrefix = ($ProjectSettings.parameterFileTokens.localTokens | Where-Object { $_.name -eq 'namePrefix' }).value + $serviceShort = ([regex]::Match($rawContent, "(?m)^param serviceShort string = '(.+)'\s*$")).Captures.Groups[1].Value - if ($SectionsToRefresh -contains 'Parameters') { - # Handle [Parameters] section - # =========================== - $inputObject = @{ - ReadMeFileContent = $readMeFileContent - TemplateFileContent = $templateFileContent - currentFolderPath = (Split-Path $TemplateFilePath -Parent) - } - $readMeFileContent = Set-ParametersSection @inputObject - } + $rawBicepExampleString = ($rawBicepExample | Out-String) + $rawBicepExampleString = $rawBicepExampleString -replace '\$\{serviceShort\}', $serviceShort + $rawBicepExampleString = $rawBicepExampleString -replace '\$\{namePrefix\}', $namePrefix + $rawBicepExampleString = $rawBicepExampleString -replace '(?m):\s*location\s*$', ': ''''' - if ($SectionsToRefresh -contains 'Outputs') { - # Handle [Outputs] section - # ======================== - $inputObject = @{ - ReadMeFileContent = $readMeFileContent - TemplateFileContent = $templateFileContent - } - $readMeFileContent = Set-OutputsSection @inputObject - } + # [3/6] Format header, remove scope property & any empty line + $rawBicepExample = $rawBicepExampleString -split '\n' + $rawBicepExample[0] = "module $resourceType './$resourceTypeIdentifier/deploy.bicep = {" + $rawBicepExample = $rawBicepExample | Where-Object { $_ -notmatch 'scope: *' } | Where-Object { -not [String]::IsNullOrEmpty($_) } - $isTopLevelModule = $TemplateFilePath.Replace('\', '/').Split('/modules/')[1].Split('/').Count -eq 3 # //deploy.* - if ($SectionsToRefresh -contains 'Deployment examples' -and $isTopLevelModule) { - # Handle [Deployment examples] section - # =================================== - $inputObject = @{ - ReadMeFileContent = $readMeFileContent - TemplateFilePath = $TemplateFilePath - TemplateFileContent = $templateFileContent - } - $readMeFileContent = Set-DeploymentExamplesSection @inputObject - } + # [4/6] Extract param block + $rawBicepExampleArray = $rawBicepExample -split '\n' + $moduleDeploymentPropertyIndent = ([regex]::Match($rawBicepExampleArray[1], '^(\s+).*')).Captures.Groups[1].Value.Length + $paramsStartIndex = ($rawBicepExampleArray | Select-String ("^[\s]{$moduleDeploymentPropertyIndent}params:[\s]*\{") | ForEach-Object { $_.LineNumber - 1 })[0] + 1 + $paramsEndIndex = ($rawBicepExampleArray[($paramsStartIndex + 1)..($rawBicepExampleArray.Count)] | Select-String "^[\s]{$moduleDeploymentPropertyIndent}\}" | ForEach-Object { $_.LineNumber - 1 })[0] + $paramsStartIndex + $paramBlock = ($rawBicepExampleArray[$paramsStartIndex..$paramsEndIndex] | Out-String).TrimEnd() - if ($SectionsToRefresh -contains 'Navigation') { - # Handle [Navigation] section - # =================================== - $inputObject = @{ - ReadMeFileContent = $readMeFileContent - } - $readMeFileContent = Set-TableOfContent @inputObject - } + # [5/6] Convert Bicep parameter block to JSON parameter block to enable processing + $conversionInputObject = @{ + BicepParamBlock = $paramBlock + } + $paramsInJSONFormat = ConvertTo-FormattedJSONParameterObject @conversionInputObject - Write-Verbose 'New content:' - Write-Verbose '============' - Write-Verbose ($readMeFileContent | Out-String) + # [6/6] Convert JSON parameters back to Bicep and order & format them + $conversionInputObject = @{ + JSONParameters = $paramsInJSONFormat + RequiredParametersList = $RequiredParametersList + } + $bicepExample = ConvertTo-FormattedBicep @conversionInputObject + + # --------------------- # + # Add Bicep example # + # --------------------- # + if ($addBicep) { + + $formattedBicepExample = $rawBicepExample[0..($paramsStartIndex - 1)] + ($bicepExample -split '\n') + $rawBicepExample[($paramsEndIndex + 1)..($rawBicepExample.Count)] + + $SectionContent += @( + '', + '
' + '' + 'via Bicep module' + '' + '```bicep', + ($formattedBicepExample | ForEach-Object { "$_" }).TrimEnd(), + '```', + '', + '
', + '

' + ) + } + + # -------------------- # + # Add JSON example # + # -------------------- # + if ($addJson) { + + # [1/2] Get all parameters from the parameter object and order them recursively + $orderingInputObject = @{ + ParametersJSON = $paramsInJSONFormat | ConvertTo-Json -Depth 99 + RequiredParametersList = $RequiredParametersList + } + $orderedJSONExample = Build-OrderedJSONObject @orderingInputObject - if ($PSCmdlet.ShouldProcess("File in path [$ReadMeFilePath]", 'Overwrite')) { - Set-Content -Path $ReadMeFilePath -Value $readMeFileContent -Force -Encoding 'utf8' - Write-Verbose "File [$ReadMeFilePath] updated" -Verbose - } -} # [2/2] Create the final content block $SectionContent += @( '', From 008b3e4e09e233de32ae06a3e37220c9c7eeffa8 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Thu, 21 Jul 2022 20:04:43 +0200 Subject: [PATCH 13/14] Updated to latest --- utilities/tools/Set-ModuleReadMe.ps1 | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/utilities/tools/Set-ModuleReadMe.ps1 b/utilities/tools/Set-ModuleReadMe.ps1 index 219c0319c4..5d0c1b7c0f 100644 --- a/utilities/tools/Set-ModuleReadMe.ps1 +++ b/utilities/tools/Set-ModuleReadMe.ps1 @@ -768,6 +768,9 @@ function Set-DeploymentExamplesSection { [string] $SectionStartIdentifier = '## Deployment examples' ) + # Load used function(s) + . (Join-Path (Split-Path $PSScriptRoot -Parent) 'pipelines' 'sharedScripts' 'Get-ModuleTestFileList.ps1') + # Process content $SectionContent = [System.Collections.ArrayList]@( 'The following module usage examples are retrieved from the content of the files hosted in the module''s `.test` folder.', @@ -779,7 +782,7 @@ function Set-DeploymentExamplesSection { $moduleRoot = Split-Path $TemplateFilePath -Parent $resourceTypeIdentifier = $moduleRoot.Replace('\', '/').Split('/modules/')[1].TrimStart('/') $resourceType = $resourceTypeIdentifier.Split('/')[1] - $testFilePaths = (Get-ChildItem (Join-Path -Path $moduleRoot -ChildPath '.test') -File).FullName | Where-Object { $_ -match '.+\.[bicep|json]' } + $testFilePaths = Get-ModuleTestFileList -ModulePath $moduleRoot | ForEach-Object { Join-Path $moduleRoot $_ } $RequiredParametersList = $TemplateFileContent.parameters.Keys | Where-Object { $TemplateFileContent.parameters[$_].Keys -notcontains 'defaultValue' } | Sort-Object @@ -794,7 +797,11 @@ function Set-DeploymentExamplesSection { $rawContent = Get-Content -Path $testFilePath -Encoding 'utf8' | Out-String # Format example header - $exampleTitle = ((Split-Path $testFilePath -LeafBase) -replace '\.', ' ') -replace ' parameters', '' + if ((Split-Path (Split-Path $testFilePath -Parent) -Leaf) -ne '.test') { + $exampleTitle = Split-Path (Split-Path $testFilePath -Parent) -Leaf + } else { + $exampleTitle = ((Split-Path $testFilePath -LeafBase) -replace '\.', ' ') -replace ' parameters', '' + } $TextInfo = (Get-Culture).TextInfo $exampleTitle = $TextInfo.ToTitleCase($exampleTitle) $SectionContent += @( @@ -811,7 +818,7 @@ function Set-DeploymentExamplesSection { # ------------------------- # # [1/6] Search for the relevant parameter start & end index - $bicepTestStartIndex = $rawContentArray.IndexOf("module testDeployment '../deploy.bicep' = {") + $bicepTestStartIndex = $rawContentArray.IndexOf("module testDeployment '../../deploy.bicep' = {") $bicepTestEndIndex = $bicepTestStartIndex do { @@ -821,12 +828,11 @@ function Set-DeploymentExamplesSection { $rawBicepExample = $rawContentArray[$bicepTestStartIndex..$bicepTestEndIndex] # [2/6] Replace placeholders - $namePrefix = ($ProjectSettings.parameterFileTokens.localTokens | Where-Object { $_.name -eq 'namePrefix' }).value $serviceShort = ([regex]::Match($rawContent, "(?m)^param serviceShort string = '(.+)'\s*$")).Captures.Groups[1].Value $rawBicepExampleString = ($rawBicepExample | Out-String) $rawBicepExampleString = $rawBicepExampleString -replace '\$\{serviceShort\}', $serviceShort - $rawBicepExampleString = $rawBicepExampleString -replace '\$\{namePrefix\}', $namePrefix + $rawBicepExampleString = $rawBicepExampleString -replace '\$\{namePrefix\}', '' # Replacing with empty to not expose prefix and avoid potential deployment conflicts $rawBicepExampleString = $rawBicepExampleString -replace '(?m):\s*location\s*$', ': ''''' # [3/6] Format header, remove scope property & any empty line From 4b4cc6be121aca5f269f436cebb0b38d19525fe5 Mon Sep 17 00:00:00 2001 From: Alexander Sehr Date: Wed, 10 Aug 2022 17:01:32 +0200 Subject: [PATCH 14/14] Update to latest --- utilities/tools/Set-ModuleReadMe.ps1 | 88 +++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/utilities/tools/Set-ModuleReadMe.ps1 b/utilities/tools/Set-ModuleReadMe.ps1 index 5d0c1b7c0f..2afba523ac 100644 --- a/utilities/tools/Set-ModuleReadMe.ps1 +++ b/utilities/tools/Set-ModuleReadMe.ps1 @@ -1,4 +1,4 @@ -#requires -version 6.0 +#requires -version 6.0 <# .SYNOPSIS @@ -303,6 +303,80 @@ function Set-OutputsSection { return $updatedFileContent } +<# +.SYNOPSIS +Add module references (cross-references) to the module's readme + +.DESCRIPTION +Add module references (cross-references) to the module's readme. This includes both local (i.e., file path), as well as remote references (e.g., ACR) + +.PARAMETER TemplateFileContent +Mandatory. The template file content object to crawl data from + +.PARAMETER ReadMeFileContent +Mandatory. The readme file content array to update + +.PARAMETER SectionStartIdentifier +Optional. The identifier of the 'outputs' section. Defaults to '## Cross-referenced modules' + +.EXAMPLE +Set-CrossReferencesSection -TemplateFileContent @{ resource = @{}; ... } -ReadMeFileContent @('# Title', '', '## Section 1', ...) +Update the given readme file's 'Cross-referenced modules' section based on the given template file content +#> +function Set-CrossReferencesSection { + + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory)] + [hashtable] $TemplateFileContent, + + [Parameter(Mandatory)] + [object[]] $ReadMeFileContent, + + [Parameter(Mandatory = $false)] + [string] $SectionStartIdentifier = '## Cross-referenced modules' + ) + + . (Join-Path (Split-Path $PSScriptRoot -Parent) 'tools' 'Get-CrossReferencedModuleList.ps1') + + $moduleRoot = Split-Path $TemplateFilePath -Parent + $resourceTypeIdentifier = $moduleRoot.Replace('\', '/').Split('/modules/')[1].TrimStart('/') + + # Process content + $SectionContent = [System.Collections.ArrayList]@( + 'This section gives you an overview of all local-referenced module files (i.e., other CARML modules that are referenced in this module) and all remote-referenced files (i.e., Bicep modules that are referenced from a Bicep Registry or Template Specs).', + '', + '| Reference | Type |', + '| :-- | :-- |' + ) + + $dependencies = (Get-CrossReferencedModuleList)[$resourceTypeIdentifier] + + if ($dependencies.Keys -contains 'localPathReferences' -and $dependencies['localPathReferences']) { + foreach ($reference in ($dependencies['localPathReferences'] | Sort-Object)) { + $SectionContent += ("| ``{0}`` | {1} |" -f $reference, 'Local reference') + } + } + + if ($dependencies.Keys -contains 'remoteReferences' -and $dependencies['remoteReferences']) { + foreach ($reference in ($dependencies['remoteReferences'] | Sort-Object)) { + $SectionContent += ("| ``{0}`` | {1} |" -f $reference, 'Remote reference') + } + } + + if ($SectionContent.Count -eq 4) { + # No content was added, adding placeholder + $SectionContent = @('_None_') + + } + + # Build result + if ($PSCmdlet.ShouldProcess('Original file with new output content', 'Merge')) { + $updatedFileContent = Merge-FileWithNewContent -oldContent $ReadMeFileContent -newContent $SectionContent -SectionStartIdentifier $SectionStartIdentifier -contentType 'none' + } + return $updatedFileContent +} + <# .SYNOPSIS Add comments to indicate required & non-required parameters to the given Bicep example @@ -1223,6 +1297,7 @@ function Set-ModuleReadMe { 'Resource Types', 'Parameters', 'Outputs', + 'CrossReferences', 'Template references', 'Navigation', 'Deployment examples' @@ -1231,6 +1306,7 @@ function Set-ModuleReadMe { 'Resource Types', 'Parameters', 'Outputs', + 'CrossReferences', 'Template references', 'Navigation', 'Deployment examples' @@ -1349,6 +1425,16 @@ function Set-ModuleReadMe { $readMeFileContent = Set-OutputsSection @inputObject } + if ($SectionsToRefresh -contains 'CrossReferences') { + # Handle [CrossReferences] section + # ======================== + $inputObject = @{ + ReadMeFileContent = $readMeFileContent + TemplateFileContent = $templateFileContent + } + $readMeFileContent = Set-CrossReferencesSection @inputObject + } + $isTopLevelModule = $TemplateFilePath.Replace('\', '/').Split('/modules/')[1].Split('/').Count -eq 3 # //deploy.* if ($SectionsToRefresh -contains 'Deployment examples' -and $isTopLevelModule) { # Handle [Deployment examples] section