utilities/tools/Get-ModulesFeatureOutline.ps1 (215 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 ModuleFolderPath Optional. The path to the modules. .PARAMETER ReturnMarkdown Optional. Instead of returning the list of objects, instead format them into a markdown table and return it as a string. .PARAMETER BreakMarkdownModuleNameAt Optional. When `ReturnMarkdown` is set to true 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 OnlyTopLevel Optional. Only consider top-level modules (that is, no child-modules). .PARAMETER AddStatusBadges Optional. Add status badges for top-level modules as a column .PARAMETER RepositoryName Optional. The name of the repository the code resides in. Required if 'AddStatusBadges' is 'true' .PARAMETER Organization Optional. The name of the Organization the code resides in. Required if 'AddStatusBadges' is 'true' .PARAMETER Environment Optional. The DevOps environment to generate the status badges for. Required if 'AddStatusBadges' is 'true'. .PARAMETER ProjectName Optional. The project the repository is hosted in. Required if 'AddStatusBadges' is 'true' and the 'environment' is 'ADO' .EXAMPLE Get-ModulesFeatureOutline Get an outline of all modules in the default module path. .EXAMPLE Get-ModulesFeatureOutline -ReturnMarkdown -OnlyTopLevel Get an outline of top-level modules in the default module path, formatted in a markdown table. .EXAMPLE Get-ModulesFeatureOutline -ReturnMarkdown -BreakMarkdownModuleNameAt 2 Get an outline of all modules in the default module path, formatted in a markdown table - with the module name column split after the top-level (i.e., <ProviderNamespace>/<ResourceType) .EXAMPLE Get-ModulesFeatureOutline -ReturnMarkdown -BreakMarkdownModuleNameAt 2 -AddStatusBadges -Environment 'GitHub' -RepositoryName 'ResourceModules' -Organization 'Azure' Get an outline of all modules in the default module path, formatted in a markdown table - with the module name column split after the top-level (i.e., <ProviderNamespace>/<ResourceType). Further, include the status badges for GitHub into the table. .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 { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string] $ModuleFolderPath = (Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'modules'), [Parameter(Mandatory = $false)] [switch] $ReturnMarkdown, [Parameter(Mandatory = $false)] [int] $BreakMarkdownModuleNameAt = 1, [Parameter(Mandatory = $false)] [switch] $OnlyTopLevel, [Parameter(Mandatory = $false)] [switch] $AddStatusBadges, [Parameter(Mandatory = $false)] [ValidateSet('GitHub', 'ADO')] [string] $Environment, [Parameter(Mandatory = $false)] [string] $RepositoryName, [Parameter(Mandatory = $false)] [string] $Organization, [Parameter(Mandatory = $false)] [string] $ProjectName = '' ) # Load external functions . (Join-Path $PSScriptRoot 'helper' 'Get-PipelineStatusUrl.ps1') . (Join-Path (Split-Path $PSScriptRoot) 'pipelines' 'sharedScripts' 'helper' 'Get-SpecsAlignedResourceName.ps1') if ($OnlyTopLevel) { $moduleTemplatePaths = (Get-ChildItem $ModuleFolderPath -Recurse -Filter 'main.bicep' -Depth 2).FullName } else { $moduleTemplatePaths = (Get-ChildItem $ModuleFolderPath -Recurse -Filter 'main.bicep').FullName } #################### # Collect data # #################### $moduleData = [System.Collections.ArrayList]@() $summaryData = [ordered]@{ supportsRBAC = 0 supportsLocks = 0 supportsTags = 0 supportsDiagnostics = 0 supportsEndpoints = 0 supportsPipDeployment = 0 numberOfChildren = 0 numberOfLines = 0 } foreach ($moduleTemplatePath in $moduleTemplatePaths) { $fullResourcePath = (((Split-Path $moduleTemplatePath -Parent) -replace '\\', '/') -split '/modules/')[1] $moduleContentArray = Get-Content -Path $moduleTemplatePath $moduleContentString = Get-Content -Path $moduleTemplatePath -Raw $moduleDataItem = [ordered]@{ Module = $fullResourcePath } # Status Badge $isTopLevelModule = ($fullResourcePath -split '/').Count -eq 2 if ($AddStatusBadges -and $isTopLevelModule) { $specsAlignedResourceName = (Get-SpecsAlignedResourceName -ResourceIdentifier $fullResourcePath).ToLower() $provider = ($specsAlignedResourceName -split '/')[0] -replace 'Microsoft', 'ms' $resourceType = ($specsAlignedResourceName -split '/')[1] $statusInputObject = @{ RepositoryName = $RepositoryName Organization = $Organization Environment = $Environment ProjectName = $ProjectName PipelineFileName = ((('{0}.{1}.yml' -f $provider, $resourceType) -replace '-', '') -replace '/', '.') PipelineFolderPath = $Environment -eq 'GitHub' ? (Join-Path '.github' 'workflows') : (Join-Path '.azuredevops' 'modulePipelines') } $moduleDataItem['Status'] = Get-PipelineStatusUrl @statusInputObject } # Supports RBAC if ([regex]::Match($moduleContentString, '(?m)^\s*param roleAssignments array\s*=.+').Success) { $summaryData.supportsRBAC++ $moduleDataItem['RBAC'] = $true } else { $moduleDataItem['RBAC'] = $false } # Supports Locks if ([regex]::Match($moduleContentString, '(?m)^\s*param lock string\s*=.+').Success) { $summaryData.supportsLocks++ $moduleDataItem['Locks'] = $true } else { $moduleDataItem['Locks'] = $false } # Supports Tags if ([regex]::Match($moduleContentString, '(?m)^\s*param tags object\s*=.+').Success) { $summaryData.supportsTags++ $moduleDataItem['Tags'] = $true } else { $moduleDataItem['Tags'] = $false } # Supports Diagnostics if ([regex]::Match($moduleContentString, '(?m)^\s*param diagnosticWorkspaceId string\s*=.+').Success) { $summaryData.supportsDiagnostics++ $moduleDataItem['Diag'] = $true } else { $moduleDataItem['Diag'] = $false } # Supports Private Endpoints if ([regex]::Match($moduleContentString, '(?m)^\s*param privateEndpoints array\s*=.+').Success) { $summaryData.supportsEndpoints++ $moduleDataItem['PE'] = $true } else { $moduleDataItem['PE'] = $false } # Supports PIPs if ([regex]::Match($moduleContentString, '(?m)^\s*param publicIPAddressObject object\s*=.+').Success) { $summaryData.supportsPipDeployment++ $moduleDataItem['PIP'] = $true } else { $moduleDataItem['PIP'] = $false } # Number of children $childFolderPaths = (Get-ChildItem -Path (Split-Path $moduleTemplatePath -Parent) -Recurse -Directory).FullName | Where-Object { $_ -and (Split-Path $_ -Leaf) -match '^\w+' } $levelsOfNesting = @() foreach ($childFolderPath in $childFolderPaths) { $simplifiedPath = $childFolderPath.Replace('\', '/').split("$fullResourcePath/")[1] $levelsOfNesting += ($simplifiedPath -split '/').Count } $groupedNesting = $levelsOfNesting | Group-Object | Sort-Object -Property 'Name' $numberOfChildrenFormatted = '[{0}]' -f (($groupedNesting | ForEach-Object { 'L{0}:{1}' -f $_.Name, $_.Count }) -join ', ') $moduleDataItem['# children'] = $numberOfChildrenFormatted $groupedNesting | ForEach-Object { $summaryData.numberOfChildren += $_.Count } # Number of lines $numberOfLines = ($moduleContentArray | Where-Object { -not [String]::IsNullOrEmpty($_) }).Count + 1 $summaryData.numberOfLines += $numberOfLines $moduleDataItem['# lines'] = $numberOfLines # Result $moduleData += $moduleDataItem } ####################### # Generate output # ####################### if ($ReturnMarkdown) { $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', ':white_check_mark:' $line = $line -replace 'False', '' $line = $line -replace '\[\]', '' $markdownTable += $line $counter++ } $markdownTable += '| Sum | | | {0} |' -f (($summaryData.Keys | ForEach-Object { $summaryData[$_] }) -join ' | ') return $markdownTable | Out-String } else { return @{ data = $moduleData | ForEach-Object { [PSCustomObject] @{ Module = $_.Module 'RBAC' = $_.'RBAC' 'Locks' = $_.'Locks' 'Tags' = $_.'Tags' 'Diag' = $_.'Diag' 'PE' = $_.'PE' 'PIP' = $_.'PIP' '# children' = $_.'# children' '# lines' = $_.'# lines' } } sum = $summaryData } } }