utilities/tools/platform/helper/Get-ModulesFeatureOutline.ps1 (272 lines of code) (raw):

<# .SYNOPSIS Get an outline of all modules features for each module contained in the given path .DESCRIPTION Get a list of objects that outline of all modules features for each module contained in the given path (for example child-modules, RBAC, Private Endpoints, etc.) NOTE: Currently only supports modules using the Bicep DSL .PARAMETER ModulesFolderPath Mandatory. The path to the modules. .PARAMETER ModulesRepoRootPath Mandatory. The path to the root of the repository containing the modules. .PARAMETER ReturnFormat Optional. Control the result format. Supports 'object', a CSV with columns containing the data, or a markdown table containing the data .PARAMETER BreakMarkdownModuleNameAt Optional. When `ReturnFormat` is set to 'Markdown' you can use this number to control if & where you'd want to line break the ModuleName column. Defaults to 1 (i.e., right after the provider namepsace). .PARAMETER SearchDepth Optional. Control in which depth to search for `main.bicep` files in the given 'ModulesRepoRootPath' .PARAMETER ColumnsToInclude Optional. An array that controls which columns / data points should be added to the result object. Defaults to all columns. The following columns are available: - Status: Adds a column with the workflow status badge for the module - RBAC: Adds a column indicating if the module supports RBAC - Locks: Adds a column indicating if the module supports Locks - Tags: Adds a column indicating if the module supports Tags - Diag: Adds a column indicating if the module supports Diagnostic Settings - PE: Adds a column indicating if the module supports Private Endpoints - PIP: Adds a column indicating if the module supports Public IP Addresses - CMK: Adds a column indicating if the module supports Customer Managed Keys - Identity: Adds a column indicating if the module supports managed identities .PARAMETER RepositoryName Optional. The name of the repository the code resides in. Required if 'ColumnsToInclude.Status' is 'true' .PARAMETER Organization Optional. The name of the Organization the code resides in. Required if 'ColumnsToInclude.Status' is 'true' .EXAMPLE Get-ModulesFeatureOutline -ReturnFormat 'Markdown' -SearchDepth 2 -ModulesFolderPath 'bicep-registry-modules/avm/res' -ModulesRepoRootPath 'bicep-registry-modules' Get an outline of top-level (from 'res' 2 level down) modules in the 'bicep-registry-modules/avm/res' folder path, formatted in a markdown table. .EXAMPLE Get-ModulesFeatureOutline -ReturnFormat 'Markdown' -BreakMarkdownModuleNameAt 2 -ModulesFolderPath 'bicep-registry-modules/avm/res' -ModulesRepoRootPath 'bicep-registry-modules' Get an outline of all modules in the 'bicep-registry-modules/avm/res' folder path, formatted in a markdown table - with the module name column split after the top-level (i.e., <ProviderNamespace>/<ResourceType) .EXAMPLE Get-ModulesFeatureOutline -ReturnFormat 'Markdown' -BreakMarkdownModuleNameAt 2 -RepositoryName 'bicep-registry-modules' -Organization 'Azure' -ModulesFolderPath 'bicep-registry-modules/avm/res' -ModulesRepoRootPath 'bicep-registry-modules' Get an outline of all modules in the 'bicep-registry-modules/avm/res' folder path, formatted in a markdown table - with the module name column split after the top-level (i.e., <ProviderNamespace>/<ResourceType). .EXAMPLE Get-ModulesFeatureOutline -ReturnFormat 'CSV' -ColumnsToInclude @( 'Status', 'PE' ) -RepositoryName 'bicep-registry-modules' -Organization 'Azure' -ModulesFolderPath 'bicep-registry-modules/avm' -ModulesRepoRootPath 'bicep-registry-modules' -SearchDepth 2 Get an outline of all modules in the 'bicep-registry-modules/avm' folder path, formatted in a CSV format - with only the Columns 'Status' & 'PE'. .NOTES Children (if any) are displayed in format `[L1:5, L2:4, L3:1]`. Each item (separated via ',') shows the level of nesting in the front (e.g. L1) and the number of children in this level (separated by a colon ':'). In the above example, the module has 5 direct children, 4 of them have direct children themselves and 1 of them has 1 more child. #> function Get-ModulesFeatureOutline { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', Justification = 'It has 3 different output types, not one. It''s a false-positive.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', '', Justification = 'For Join-Path it''s very difficult to read the cmdlet without positional parameters.')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $ModulesFolderPath, [Parameter(Mandatory = $true)] [string] $ModulesRepoRootPath, [Parameter(Mandatory = $false)] [ValidateSet('Status', 'RBAC', 'Locks', 'Tags', 'Diag', 'PE', 'PIP', 'CMK', 'Identity')] [string[]] $ColumnsToInclude = @( 'Status', 'RBAC', 'Locks', 'Tags', 'Diag', 'PE', 'PIP', 'CMK', 'Identity' ), [Parameter(Mandatory = $false)] [ValidateSet('Object', 'Markdown', 'CSV')] [string] $ReturnFormat = 'Object', [Parameter(Mandatory = $false)] [int] $BreakMarkdownModuleNameAt = 2, [Parameter(Mandatory = $false)] [int] $SearchDepth, [Parameter(Mandatory = $false)] [string] $RepositoryName = 'bicep-registry-modules', [Parameter(Mandatory = $false)] [string] $Organization = 'Azure' ) # Load external functions . (Join-Path $PSScriptRoot 'Get-PipelineStatusUrl.ps1') . (Join-Path $PSScriptRoot 'Get-PipelineFileName.ps1') $childInput = @{ Path = $ModulesFolderPath Recurse = $true File = $true Filter = 'main.bicep' } if ($SearchDepth) { $childInput.Depth = $SearchDepth } $moduleTemplatePaths = (Get-ChildItem @childInput).FullName #################### # Collect data # #################### $moduleData = [System.Collections.ArrayList]@() $summaryData = [ordered]@{} if ($ColumnsToInclude -contains 'RBAC') { $summaryData.supportsRBAC = 0 } if ($ColumnsToInclude -contains 'Locks') { $summaryData.supportsLocks = 0 } if ($ColumnsToInclude -contains 'Tags') { $summaryData.supportsTags = 0 } if ($ColumnsToInclude -contains 'Diag') { $summaryData.supportsDiagnostics = $_.Diag } if ($ColumnsToInclude -contains 'PE') { $summaryData.supportsEndpoints = 0 } if ($ColumnsToInclude -contains 'PIP') { $summaryData.supportsPipDeployment = 0 } if ($ColumnsToInclude -contains 'CMK') { $summaryData.supportsCMKDeployment = 0 } if ($ColumnsToInclude -contains 'Identity') { $summaryData.supportsIdentityDeployment = 0 } foreach ($moduleTemplatePath in $moduleTemplatePaths) { $fullResourcePath = (((Split-Path $moduleTemplatePath -Parent) -replace '\\', '/') -split '/avm/')[1] $moduleContentString = Get-Content -Path $moduleTemplatePath -Raw $moduleDataItem = [ordered]@{ Module = $fullResourcePath } # Status Badge $moduleFolderPath = Split-Path $moduleTemplatePath -Parent $relativeFolderPath = Join-Path 'avm' ($moduleFolderPath -split '[\/|\\]{1}avm[\/|\\]{1}')[1] $resourceTypeIdentifier = ($moduleFolderPath -split '[\/|\\]{1}avm[\/|\\]{1}(res|ptn|utl)[\/|\\]{1}')[2] -replace '\\', '/' # avm/res/<provider>/<resourceType> $isTopLevelModule = ($resourceTypeIdentifier -split '[\/|\\]').Count -eq 2 if (($ColumnsToInclude -contains 'Status') -and $isTopLevelModule) { $statusInputObject = @{ RepositoryName = $RepositoryName Organization = $Organization PipelineFileName = Get-PipelineFileName -ResourceIdentifier $relativeFolderPath WorkflowsFolderPath = Join-Path $ModulesRepoRootPath '.github' 'workflows' } $moduleDataItem['Status'] = Get-PipelineStatusUrl @statusInputObject } # Supports RBAC if ($ColumnsToInclude -contains 'RBAC') { if ([regex]::Match($moduleContentString, '(?m)^\s*param roleAssignments roleAssignment.*Type').Success) { $summaryData.supportsRBAC++ $moduleDataItem['RBAC'] = $true } else { $moduleDataItem['RBAC'] = $false } } # Supports Locks if ($ColumnsToInclude -contains 'Locks') { if ([regex]::Match($moduleContentString, '(?m)^\s*param lock lock.*Type').Success) { $summaryData.supportsLocks++ $moduleDataItem['Locks'] = $true } else { $moduleDataItem['Locks'] = $false } } # Supports Tags if ($ColumnsToInclude -contains 'Tags') { if ([regex]::Match($moduleContentString, '(?m)^\s*param tags object\?').Success) { $summaryData.supportsTags++ $moduleDataItem['Tags'] = $true } else { $moduleDataItem['Tags'] = $false } } # Supports Diagnostics if ($ColumnsToInclude -contains 'Diag') { if ([regex]::Match($moduleContentString, '(?m)^\s*param diagnosticSettings diagnosticSetting.*Type').Success) { $summaryData.supportsDiagnostics++ $moduleDataItem['Diag'] = $true } else { $moduleDataItem['Diag'] = $false } } # Supports Private Endpoints if ($ColumnsToInclude -contains 'PE') { if ([regex]::Match($moduleContentString, '(?m)^\s*param privateEndpoints privateEndpoint.*Type').Success) { $summaryData.supportsEndpoints++ $moduleDataItem['PE'] = $true } else { $moduleDataItem['PE'] = $false } } # Supports PIPs if ($ColumnsToInclude -contains 'PIP') { if ([regex]::Match($moduleContentString, '(?m)^\s*param publicIPAddressObject object\s*=.+').Success) { $summaryData.supportsPipDeployment++ $moduleDataItem['PIP'] = $true } else { $moduleDataItem['PIP'] = $false } } # Supports CMK if ($ColumnsToInclude -contains 'CMK') { if ([regex]::Match($moduleContentString, '(?m)^\s*param customerManagedKey customerManagedKey.*Type').Success) { $summaryData.supportsCMKDeployment++ $moduleDataItem['CMK'] = $true } else { $moduleDataItem['CMK'] = $false } } # Supports Identity if ($ColumnsToInclude -contains 'Identity') { if ([regex]::Match($moduleContentString, '(?m)^\s*param managedIdentities managedIdentit.*Type').Success) { $summaryData.supportsIdentityDeployment++ $moduleDataItem['Identity'] = $true } else { $moduleDataItem['Identity'] = $false } } # Result $moduleData += $moduleDataItem } ####################### # Generate output # ####################### switch ($ReturnFormat) { 'Object' { return @{ data = $moduleData | ForEach-Object { $resultObject = @{ Module = $_.Module } if ($ColumnsToInclude -contains 'Status') { $resultObject.Status = $_.Status } if ($ColumnsToInclude -contains 'RBAC') { $resultObject.RBAC = $_.RBAC } if ($ColumnsToInclude -contains 'Locks') { $resultObject.Locks = $_.Locks } if ($ColumnsToInclude -contains 'Tags') { $resultObject.Tags = $_.Tags } if ($ColumnsToInclude -contains 'Diag') { $resultObject.Diag = $_.Diag } if ($ColumnsToInclude -contains 'PE') { $resultObject.PE = $_.PE } if ($ColumnsToInclude -contains 'PIP') { $resultObject.PIP = $_.PIP } if ($ColumnsToInclude -contains 'CMK') { $resultObject.CMK = $_.CMK } if ($ColumnsToInclude -contains 'Identity') { $resultObject.Identity = $_.Identity } # Return result [PSCustomObject] $resultObject } sum = $summaryData } } 'Markdown' { $markdownTable = [System.Collections.ArrayList]@( '| # | {0} |' -f ($moduleData[0].Keys -join ' | ') '| - | {0} |' -f (($moduleData[0].Keys | ForEach-Object { '-' }) -join ' | ' ) ) # Format module identifier foreach ($module in $moduleData) { $identifierParts = $module.Module.Replace('\', '/').split('/') if ($identifierParts.Count -gt $BreakMarkdownModuleNameAt) { $topLevelIdentifier = $identifierParts[0..($BreakMarkdownModuleNameAt - 1)] -join '/' $module.Module = '{0}<p>{1}' -f $topLevelIdentifier, ($module.Module -replace "$topLevelIdentifier/", '') } } # Add table data $counter = 1 foreach ($module in ($moduleData | Sort-Object { $_.Module })) { $line = '| {0} | {1} |' -f $counter, (($moduleData[0].Keys | ForEach-Object { $module[$_] }) -join ' | ') $line = $line -replace 'True', '✅' $line = $line -replace 'False', '' $line = $line -replace '\[\]', '' $markdownTable += $line $counter++ } if ($summaryData.Keys.Count -gt 0) { $markdownTable += '| Sum | | | {0} |' -f (($summaryData.Keys | ForEach-Object { $summaryData[$_] }) -join ' | ') } return $markdownTable | Out-String } 'CSV' { $csv = [System.Collections.ArrayList]@( ('#,{0}' -f ($moduleData[0].Keys -join ',') )) # Add CSV data $counter = 1 foreach ($module in ($moduleData | Sort-Object { $_.Module })) { $line = '{0},{1}' -f $counter, (($moduleData[0].Keys | ForEach-Object { $module[$_] }) -join ',') $line = $line -replace 'True', '✅' $line = $line -replace 'False', '' $line = $line -replace '\[\]', '' $csv += $line $counter++ } if ($summaryData.Keys.Count -gt 0) { $csv += 'Sum,{0},{1}' -f (($summaryData.Keys -contains 'Status') ? ',' : ''), (($summaryData.Keys | ForEach-Object { $summaryData[$_] }) -join ',') } return $csv } } }