utilities/pipelines/platform/Invoke-AvmJsonModuleIndexGeneration.ps1 (203 lines of code) (raw):
<#
.SYNOPSIS
Creates the moduleIndex.json file for the AVM modules that is used by Visual Studio Code and other IDEs to provide the intellisense list of modules from the Bicep public registry.
.PARAMETER storageAccountName
The name of the Azure Storage Account where the moduleIndex.json file is stored. Default is 'biceplivedatasaprod'.
.PARAMETER storageAccountContainer
The name of the Azure Storage Account Blob Container where the moduleIndex.json file is stored. Default is 'bicep-cdn-live-data-container'.
.PARAMETER storageBlobName
The name of the Azure Storage Account Blob where the moduleIndex.json file is stored. Default is 'module-index'.
.PARAMETER moduleIndexJsonFilePath
The file path to save the moduleIndex.json file to. Default is 'moduleIndex.json'.
.PARAMETER prefixForLastModuleIndexJsonFile
The prefix to add to the last version of the moduleIndex.json file that is downloaded from the storage account. Default is 'last-'.
.PARAMETER prefixForCurrentGeneratedModuleIndexJsonFile
The prefix to add to the current generated moduleIndex.json file. Default is 'generated-'.
.PARAMETER doNotMergeWithLastModuleIndexJsonFileVersion
If specified, the last version of the moduleIndex.json file that is downloaded from the storage account will not be merged with the current generated moduleIndex.json file.
.DESCRIPTION
Creates the moduleIndex.json file for the AVM modules that is used by Visual Studio Code and other IDEs to provide the intellisense list of modules from the Bicep public registry.
Also has error handling to cope with a module not being published fully but will not prevent the script from completeing each time.
The script uses a merging strategy with the previous version of moduleIndex.json to ensure that the file is always up to date with the latest modules but previous versions are not removed, this can be changed by specifying the $doNotMergeWithLastModuleIndexJsonFileVersion parameter.
.EXAMPLE
Invoke-AvmJsonModuleIndexGeneration -storageAccountName '<STORAGE ACCOUNT NAME>' -storageAccountContainer '<STORAGE ACCOUNT BLOB CONTAINER NAME>' -storageBlobName '<STORAGE ACCOUNT BLOB NAME>' -moduleIndexJsonFilePath 'moduleIndex.json' -prefixForLastModuleIndexJsonFile 'last-' -prefixForCurrentGeneratedModuleIndexJsonFile 'generated-'
This example will generate the moduleIndex.json file for the AVM modules and save it to the current directory and merge it with the last version of the moduleIndex.json file that was downloaded from the storage account.
.NOTES
The function requires Azure PowerShell Storage Module (Az.Storage) to be installed and the user to be logged in to Azure.
#>
function Invoke-AvmJsonModuleIndexGeneration {
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[string] $storageAccountName = 'biceplivedatasaprod',
[Parameter(Mandatory = $false)]
[string] $storageAccountContainer = 'bicep-cdn-live-data-container',
[Parameter(Mandatory = $false)]
[string] $storageBlobName = 'module-index',
[Parameter(Mandatory = $false)]
[string] $moduleIndexJsonFilePath = 'moduleIndex.json',
[Parameter(Mandatory = $false)]
[string] $prefixForLastModuleIndexJsonFile = 'last-',
[Parameter(Mandatory = $false)]
[string] $prefixForCurrentGeneratedModuleIndexJsonFile = 'generated-',
[Parameter(Mandatory = $false)]
[switch] $doNotMergeWithLastModuleIndexJsonFileVersion
)
## Generate the new moduleIndex.json file based off the modules in the repository
$currentGeneratedModuleIndexJsonFilePath = $prefixForCurrentGeneratedModuleIndexJsonFile + $moduleIndexJsonFilePath
Write-Verbose "Generating the current generated moduleIndex.json file and saving to: $currentGeneratedModuleIndexJsonFilePath ..." -Verbose
$global:anyErrorsOccurred = $false
$global:moduleIndexData = @()
foreach ($avmModuleRoot in @('avm/res', 'avm/ptn', 'avm/utl')) {
$avmModuleGroups = (Get-ChildItem -Path $avmModuleRoot -Directory).Name
foreach ($moduleGroup in $avmModuleGroups) {
$moduleGroupPath = "$avmModuleRoot/$moduleGroup"
$moduleNames = (Get-ChildItem -Path $moduleGroupPath -Directory).Name
foreach ($moduleName in $moduleNames) {
$modulePath = "$moduleGroupPath/$moduleName"
$mainJsonPath = "$modulePath/main.json"
$tagListUrl = "https://mcr.microsoft.com/v2/bicep/$modulePath/tags/list"
Add-ModuleToAvmJsonModuleIndex -modulePath $modulePath -mainJsonPath $mainJsonPath -tagListUrl $tagListUrl
## Find child modules that contain a main.bicep, main.json, README.md and version.json file
$verifiedChildModules = @()
$possibleChildModules = (Get-ChildItem -Path $modulePath -Directory -Exclude 'tests').Name
Write-Verbose ' Checking for possible child modules...' -Verbose
if ($possibleChildModules.Count -ne 0) {
Write-Verbose " Possible child modules: $possibleChildModules" -Verbose
foreach ($possibleChildModule in $possibleChildModules) {
Write-Verbose " Processing possible child module: $possibleChildModule" -Verbose
$checkChildModuleContainsRequiredFilesForPublishing = (Test-Path -Path "$modulePath/$possibleChildModule/main.bicep") -and (Test-Path -Path "$modulePath/$possibleChildModule/main.json") -and (Test-Path -Path "$modulePath/$possibleChildModule/README.md") -and (Test-Path -Path "$modulePath/$possibleChildModule/version.json")
Write-Verbose " Child module contains required files for publishing?: $checkChildModuleContainsRequiredFilesForPublishing" -Verbose
if ($checkChildModuleContainsRequiredFilesForPublishing) {
Write-Verbose ' Add child module to array for inclusion in index JSON generation...' -Verbose
$verifiedChildModules += $possibleChildModule
}
}
} else {
Write-Verbose ' No possible child modules found for this module.' -Verbose
}
foreach ($verifiedChildModule in $verifiedChildModules) {
$childModulePath = "$modulePath/$verifiedChildModule"
$childModuleMainJsonPath = "$childModulePath/main.json"
$childModuleTagListUrl = "https://mcr.microsoft.com/v2/bicep/$childModulePath$mod/tags/list"
Add-ModuleToAvmJsonModuleIndex -modulePath $childModulePath -mainJsonPath $childModuleMainJsonPath -tagListUrl $childModuleTagListUrl
}
}
$numberOfModuleGroupsProcessed++
}
}
Write-Verbose "Processed $numberOfModuleGroupsProcessed modules groups." -Verbose
Write-Verbose "Processed $($global:moduleIndexData.Count) total modules." -Verbose
Write-Verbose "Convert moduleIndexData variable to JSON and save as 'generated-moduleIndex.json'" -Verbose
$global:moduleIndexData | ConvertTo-Json -Depth 10 | Out-File -FilePath $currentGeneratedModuleIndexJsonFilePath
## Download the current published moduleIndex.json from the storage account if the $doNotMergeWithLastModuleIndexJsonFileVersion is set to $false
if (-not $doNotMergeWithLastModuleIndexJsonFileVersion) {
try {
$lastModuleIndexJsonFilePath = $prefixForLastModuleIndexJsonFile + $moduleIndexJsonFilePath
Write-Verbose "Attempting to get last version of the moduleIndex.json from the Storage Account: $storageAccountName, Container: $storageAccountContainer, Blob: $storageBlobName and save to file: $lastModuleIndexJsonFilePath ..." -Verbose
$storageContext = New-AzStorageContext -StorageAccountName $storageAccountName -UseConnectedAccount
Get-AzStorageBlobContent -Blob $storageBlobName -Container $storageAccountContainer -Context $storageContext -Destination $lastModuleIndexJsonFilePath -Force | Out-Null
} catch {
Write-Error "Unable to retrieve moduleIndex.json file from the Storage Account: $storageAccountName, Container: $storageAccountContainer, Blob: $storageBlobName. Error: $($_.Exception.Message)" -ErrorAction 'Stop'
}
## Check if the last version of the moduleIndex.json (last-moduleIndex.json) file exists and is not empty
if (Test-Path $lastModuleIndexJsonFilePath) {
$lastModuleIndexJsonFileContent = Get-Content $lastModuleIndexJsonFilePath
if ($null -eq $lastModuleIndexJsonFileContent) {
Write-Error "The last version of the moduleIndex.json file (last-moduleIndex.json) exists but is empty. File: $lastModuleIndexJsonFilePath" -ErrorAction 'Stop'
}
Write-Verbose 'The last version of the moduleIndex.json file (last-moduleIndex.json) exists and is not empty. Proceeding...' -Verbose
}
## Merge the new moduleIndex.json file with the previous version if the $doNotMergeWithLastModuleIndexJsonFileVersion is not specified
Write-Verbose "Merging 'generated-moduleIndex.json' (new) file with 'last-moduleIndex.json' (previous) file..." -Verbose
$lastModuleIndexJsonFileContent = Get-Content $lastModuleIndexJsonFilePath
$currentGeneratedModuleIndexJsonFileContent = Get-Content $currentGeneratedModuleIndexJsonFilePath
$lastModuleIndexData = $lastModuleIndexJsonFileContent | ConvertFrom-Json -Depth 10
$currentGeneratedModuleIndexData = $currentGeneratedModuleIndexJsonFileContent | ConvertFrom-Json -Depth 10
$initialMergeOfJsonFilesData = @{}
foreach ($module in $currentGeneratedModuleIndexData) {
$initialMergeOfJsonFilesData[$module.moduleName] = $module
}
# Add modules from lastModuleIndexData to the initialMergeOfJsonFilesData hashtable, merging tags and properties if they exist in both files
foreach ($module in $lastModuleIndexData) {
if (-not $initialMergeOfJsonFilesData.ContainsKey($module.moduleName)) {
$initialMergeOfJsonFilesData[$module.moduleName] = $module
} else {
# If the module exists, merge the tags and properties
$mergedModule = $initialMergeOfJsonFilesData[$module.moduleName]
$mergedModule.tags = @(($mergedModule.tags + $module.tags) | Sort-Object -Culture 'en-US' -Unique)
# Merge properties
foreach ($property in $module.properties.PSObject.Properties) {
if (-not $mergedModule.properties.PSObject.Properties.Name.Contains($property.Name)) {
$mergedModule.properties | Add-Member -NotePropertyName $property.Name -NotePropertyValue $property.Value
}
}
}
}
# Convert the mergedModuleIndexData hashtable to an array of values (i.e., the modules)
$mergedModuleIndexData = $initialMergeOfJsonFilesData.Values
# Sort the modules by their names
$sortedMergedModuleIndexData = $mergedModuleIndexData | Sort-Object -Culture 'en-US' -Property 'moduleName'
Write-Verbose "Convert mergedModuleIndexData variable to JSON and save as 'moduleIndex.json'" -Verbose
$sortedMergedModuleIndexData | ConvertTo-Json -Depth 10 | Out-File -FilePath $moduleIndexJsonFilePath
}
if ($doNotMergeWithLastModuleIndexJsonFileVersion -eq $true) {
Write-Verbose "Convert currentGeneratedModuleIndexData variable to JSON and save as 'moduleIndex.json to overwrite it as `doNotMergeWithLastModuleIndexJsonFileVersion` was specified'" -Verbose
$global:moduleIndexData | ConvertTo-Json -Depth 10 | Out-File -FilePath $moduleIndexJsonFilePath -Force
}
return ($global:anyErrorsOccurred ? $false : $true)
}
function Add-ModuleToAvmJsonModuleIndex {
param (
[Parameter(Mandatory = $true)]
[string] $modulePath,
[Parameter(Mandatory = $false)]
[string] $mainJsonPath = "$modulePath/main.json",
[Parameter(Mandatory = $false)]
[string] $tagListUrl = "https://mcr.microsoft.com/v2/bicep/$modulePath/tags/list"
)
try {
Write-Verbose "Processing AVM Module '$modulePath'..." -Verbose
Write-Verbose " Getting available tags at '$tagListUrl'..." -Verbose
try {
$tagListResponse = Invoke-RestMethod -Uri $tagListUrl
} catch {
$global:anyErrorsOccurred = $true
Write-Error "Error occurred while accessing URL: $tagListUrl"
Write-Error "Error message: $($_.Exception.Message)"
continue
}
$tags = $tagListResponse.tags | Sort-Object -Culture 'en-US'
# Sort tags by order of semantic versioning with the latest version last
$tags = $tags | Sort-Object -Property { [semver] $_ }
$properties = [ordered]@{}
foreach ($tag in $tags) {
$gitTag = "$modulePath/$tag"
$documentationUri = "https://github.com/Azure/bicep-registry-modules/tree/$gitTag/$modulePath/README.md"
try {
$moduleMainJsonUri = "https://raw.githubusercontent.com/Azure/bicep-registry-modules/$gitTag/$mainJsonPath"
Write-Verbose " Getting available description for tag $tag via '$moduleMainJsonUri'..." -Verbose
$moduleMainJsonUriResponse = Invoke-RestMethod -Uri $moduleMainJsonUri
$description = $moduleMainJsonUriResponse.metadata.description
} catch {
$global:anyErrorsOccurred = $true
Write-Error "Error occurred while accessing description for tag $tag via '$moduleMainJsonUri'"
Write-Error "Error message: $($_.Exception.Message)"
continue
}
$properties[$tag] = [ordered]@{
description = $description
documentationUri = $documentationUri
}
}
$global:moduleIndexData += [ordered]@{
moduleName = $modulePath
tags = @($tags)
properties = $properties
}
} catch {
$global:anyErrorsOccurred = $true
Write-Error "Error message: $($_.Exception.Message)"
}
return ($global:anyErrorsOccurred ? $false : $true)
}