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 += @( '

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 - } - } - } - - $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), - '}', - '' - ) + ## ----------------------------------- ## + ## Handle by type (Bicep vs. JSON) ## + ## ----------------------------------- ## + if ((Split-Path $testFilePath -Extension) -eq '.bicep') { - # Add attribute for later correct reference - $keyVaultReferenceData | Where-Object { $_.vaultName -eq $reference.vaultName } | ForEach-Object { - $_['vaultResourceReference'] = "kv$counter" - } - } + # ------------------------- # + # Prepare Bicep to JSON # + # ------------------------- # - # 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 - } - } + # [1/6] Search for the relevant parameter start & end index + $bicepTestStartIndex = $rawContentArray.IndexOf("module testDeployment '../../deploy.bicep' = {") - # Order parameters recursively - $JSONParametersWithoutValue = ConvertTo-OrderedHashtable -JSONInputObject ($JSONParametersWithoutValue | ConvertTo-Json -Depth 99) + $bicepTestEndIndex = $bicepTestStartIndex + do { + $bicepTestEndIndex++ + } while ($rawContentArray[$bicepTestEndIndex] -ne '}') - # 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[$_] } + $rawBicepExample = $rawContentArray[$bicepTestStartIndex..$bicepTestEndIndex] - if ($orderedJSONParameters.count -eq 0) { - # Handle empty dictionaries (in case the parmaeter file was empty) - $orderedJSONParameters = @{} - } + # [2/6] Replace placeholders + $serviceShort = ([regex]::Match($rawContent, "(?m)^param serviceShort string = '(.+)'\s*$")).Captures.Groups[1].Value - $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()] + $rawBicepExampleString = ($rawBicepExample | Out-String) + $rawBicepExampleString = $rawBicepExampleString -replace '\$\{serviceShort\}', $serviceShort + $rawBicepExampleString = $rawBicepExampleString -replace '\$\{namePrefix\}', '' # Replacing with empty to not expose prefix and avoid potential deployment conflicts + $rawBicepExampleString = $rawBicepExampleString -replace '(?m):\s*location\s*$', ': ''''' - $bicepParamsArray = $contentInBicepFormat -split '\n' - $bicepParamsArray = $bicepParamsArray[1..($bicepParamsArray.count - 2)] - } + # [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($_) } - # Format params with indent - $bicepExample = $bicepParamsArray | ForEach-Object { " $_" } + # [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() - # Optional: Add comment where required & optional parameters start - # ---------------------------------------------------------------- - if ($requiredParameterNames -is [string]) { - $requiredParameterNames = @($requiredParameterNames) + # [5/6] Convert Bicep parameter block to JSON parameter block to enable processing + $conversionInputObject = @{ + BicepParamBlock = $paramBlock } + $paramsInJSONFormat = ConvertTo-FormattedJSONParameterObject @conversionInputObject - # 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' + # [6/6] Convert JSON parameters back to Bicep and order & format them + $conversionInputObject = @{ + JSONParameters = $paramsInJSONFormat + RequiredParametersList = $RequiredParametersList + } + $bicepExample = ConvertTo-FormattedBicep @conversionInputObject - # 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 Bicep example # + # --------------------- # + if ($addBicep) { - # Add a comment where the required parameters start - $bicepExampleArray = @('{0}// Required parameters' -f (' ' * $requiredParameterIndent)) + $bicepExampleArray[(0 .. ($bicepExampleArray.Count))] + $formattedBicepExample = $rawBicepExample[0..($paramsStartIndex - 1)] + ($bicepExample -split '\n') + $rawBicepExample[($paramsEndIndex + 1)..($rawBicepExample.Count)] - # Find the location if the last required parameter - $requiredParameterStartIndex = ($bicepExampleArray | Select-String ('^[\s]{0}{1}:.+' -f "{$requiredParameterIndent}", $parameterToSplitAt) | ForEach-Object { $_.LineNumber - 1 })[0] + $SectionContent += @( + '', + '
' + '' + 'via Bicep module' + '' + '```bicep', + ($formattedBicepExample | ForEach-Object { "$_" }).TrimEnd(), + '```', + '', + '
', + '

' + ) + } - # 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 # //deploy.* if ($SectionsToRefresh -contains 'Deployment examples' -and $isTopLevelModule) { # Handle [Deployment examples] section @@ -866,6 +1443,7 @@ function Set-ModuleReadMe { ReadMeFileContent = $readMeFileContent TemplateFilePath = $TemplateFilePath TemplateFileContent = $templateFileContent + ProjectSettings = $projectSettings } $readMeFileContent = Set-DeploymentExamplesSection @inputObject }