diff --git a/utilities/tools/Set-ModuleReadMe.ps1 b/utilities/tools/Set-ModuleReadMe.ps1 index 4c9810c656..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,483 @@ 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 + +.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 + + # [2/4] Add a comment where the required parameters start + $BicepParamsArray = @('{0}// Required parameters' -f (' ' * $requiredParameterIndent)) + $BicepParamsArray[(0 .. ($BicepParamsArray.Count))] + + # [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] + + # [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 + } + + # 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 + # [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] + } + } + + # [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 ($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] + } + } + + # [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 @@ -316,6 +793,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 @@ -328,8 +808,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 #> @@ -352,12 +835,15 @@ 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') + . (Join-Path (Split-Path $PSScriptRoot -Parent) 'pipelines' 'sharedScripts' 'Get-ModuleTestFileList.ps1') # Process content $SectionContent = [System.Collections.ArrayList]@( @@ -370,234 +856,297 @@ 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-ModuleTestFileList -ModulePath $moduleRoot | ForEach-Object { Join-Path $moduleRoot $_ } - $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 - $exampleTitle = ((Split-Path $testFilePath -LeafBase) -replace '\.', ' ') -replace ' parameters', '' + # Format example header + 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 += @( '
'
+ )
+ }
- # 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))]
+ # [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
- $bicepExample = $bicepExampleArray | Out-String
+ # [2/2] 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/5] Get all parameters from the parameter object
+ $JSONParametersHashTable = (ConvertFrom-Json $jsonParameterContent -AsHashtable -Depth 99).parameters
+
+ # [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' }
+
+ 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) {
+ $keysOnLevel = $JSONParameters[$parameterName].Keys
+ if ($keysOnLevel.count -eq 1 -and $keysOnLevel -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 += @(
@@ -607,7 +1156,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
@@ -736,7 +1287,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'),
@@ -746,6 +1297,7 @@ function Set-ModuleReadMe {
'Resource Types',
'Parameters',
'Outputs',
+ 'CrossReferences',
'Template references',
'Navigation',
'Deployment examples'
@@ -754,6 +1306,7 @@ function Set-ModuleReadMe {
'Resource Types',
'Parameters',
'Outputs',
+ 'CrossReferences',
'Template references',
'Navigation',
'Deployment examples'
@@ -768,10 +1321,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 | 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 | ConvertFrom-Json -AsHashtable
+ } else {
+ $templateFileContent = ConvertFrom-Json (Get-Content $TemplateFilePath -Encoding 'utf8' -Raw) -ErrorAction Stop -AsHashtable
+ }
}
}
@@ -781,6 +1339,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
@@ -858,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 #