Simpleverse.Bicep.psm1
using namespace System.Management.Automation enum LogLevel { Verbose Debug Information Warning Error } function Format-LogMessage { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [Object] $Message, [Parameter(Mandatory = $true, Position = 1)] [Alias("l")] [LogLevel] $Level ) BEGIN {} PROCESS { if ($level -eq [LogLevel]::Debug) { return "[$(Get-Date -format "yyyy-MM-dd HH:mm:ss.fff")] $($Message)" } switch ($Level) { Verbose { $prefix = 'VERBOSE' } Debug { $prefix = '' } Information { $prefix = 'INFO' } Warning { $prefix = 'WARNING' } Error { $prefix = 'ERROR' } } return "$($prefix): [$(Get-Date -format "yyyy-MM-dd HH:mm:ss.fff")] $($Message)" } END {} } function Format-Message { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [Object] $Message, [Parameter(Mandatory = $false)] [Nullable[System.Int32]] $Index ) BEGIN {} PROCESS { if ($null -eq $Index) { return "$($Message)" } else { return "[#$($index)] $($message)" } } END {} } function Write-DebugEx { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [Object] $Message ) BEGIN {} PROCESS { if ($null -eq $Message -or $Message -eq '') { return } Format-LogMessage -Message $Message -Level Debug | Write-Debug } END {} } function Write-InformationEx { <# .SYNOPSIS Writes out a message in defined colors. .DESCRIPTION Enables writing of message in defined colors as with Write-Host, but replaces Write-Host as it is not the prefered way of writing output. .PARAMETER Message Message to writeout. .PARAMETER Background Background color of the text. .PARAMETER Foreground Foreground color of the text. .PARAMETER NoNewline Terminate with an new line or not. .PARAMETER noOutput Do not override InformationAction. .EXAMPLE PS C:\> Write-InformationEx 'Message' -Foreground 'Cyan' -Background 'White' -NoNewline #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [Object]$Message, [Parameter(Mandatory = $false)] [ValidateNotNullOrWhiteSpace()] [Alias("b")] [System.ConsoleColor[]]$BackgroundColor = $Host.UI.RawUI.BackgroundColor, [Parameter(Mandatory = $false)] [Alias("f")] [ValidateNotNullOrWhiteSpace()] [System.ConsoleColor[]]$ForegroundColor = $Host.UI.RawUI.ForegroundColor, [Parameter(Mandatory = $false)] [Alias("nn")] [Switch]$NoNewline, [Parameter(Mandatory = $false)] [Alias("no")] [Switch]$noOutput ) BEGIN {} PROCESS { if ($null -eq $Message -or $Message -eq '') { return } [HostInformationMessage]$outMessage = @{ Message = Format-LogMessage -Message $Message -Level Information ForegroundColor = $ForegroundColor BackgroundColor = $BackgroundColor NoNewline = $NoNewline } if ($noOutput) { Write-Information $outMessage } else { Write-Information $outMessage -InformationAction Continue } } END {} } class BicepModule { [string] $FilePath [string] $Name BicepModule() {} BicepModule([string] $fullPath) { $this.FilePath = Resolve-Path -Relative $fullPath $this.Name = $this.FilePath -replace "^./" -replace '^.\\' -replace '\..*' } BicepModule([string] $filePath, [string] $name) { FilePath = $filePath Name = $name } } <# .SYNOPSIS Lists all modules impacted by changes in a defined commit range. .DESCRIPTION The command will output modules that have either been changed in the commit range or modules that have been impacted by the change. The module list will include * modules added, edited or renamed * modules that import or use modules added, edited or renamed The command preforms the search recursively through all detected module files to build a complete list of impacted modules. .INPUTS None. You cannot pipe objects to Add-Extension. .OUTPUTS None. .EXAMPLE PS> Get-BicepModuleChanged '*.bicep' 'd41eeb1c7c0a6a5e3f11efc175aa36b8eaae4af5..0ee2650f101237af9ad923ad2264d37b983d8bab' .LINK https://github.com/lukaferlez/Simpleverse.Bicep/blob/main/README.md #> function Get-BicepModuleChanged { Param( [Parameter(Mandatory=$true, Position=0, HelpMessage="PathSpec to grep Bicep modules to publish.")] [ValidateNotNullOrWhiteSpace()] [string] $PathSpec, [Parameter(Mandatory=$true, Position=1, HelpMessage="Commit range to check for changes.")] [ValidateNotNullOrWhiteSpace()] [string] $CommitRange, [Parameter(Mandatory=$false, HelpMessage="Exclude direct changes to files in pathSpec from being published.")] [Alias("ed")] [switch] $ExcludeDirectChanges ) Write-InformationEx "Get-BicepModuleChanged: $PathSpec - $CommitRange" -ForegroundColor Green $changedFiles = git diff-tree --no-commit-id --name-only --diff-filter=d -r $CommitRange $PathSpec Write-DebugEx "Found $($changedFiles.Count) changed files." $changedFiles | Format-Table | Out-String | Write-DebugEx $changedModules = @() foreach ($file in $changedFiles) { $changedModules += [BicepModule]::new($file) } Write-InformationEx "Get-BicepModuleChanged: Found $($changedModules.Count) changed modules." -ForegroundColor Green $changedModules | Select-Object Name, FilePath | Format-Table | Out-String | Write-InformationEx $bicepImports = Get-BicepModuleImport $PathSpec function Get-ImpactedModules2 { Param( [BicepModule] $changedModule, $imports ) Write-DebugEx "Get-ImpactedModules: $($changedModule.FilePath) - Resolving impacted modules" $impactedModules = @($changedModule) $import = $imports | Where-Object { $_.Name -eq $changedModule.FilePath } | Select-Object -First 1 if ($null -ne $import) { Write-DebugEx "Get-BicepModuleChanged: $($changedModule.FilePath) - Discovered dependecies" foreach($filePath in $import.FilePaths) { Write-DebugEx "Get-BicepModuleChanged: $($changedModule.FilePath) - Resolving impacted modules for $($filePath)" $module = [BicepModule]::new($filePath) $impactedModules += Get-ImpactedModules2 $module $imports } } return $impactedModules } $impactedModules = @() foreach($changedModule in $changedModules) { $impactedModules += Get-ImpactedModules2 $changedModule $bicepImports } $impactedModules = $impactedModules | Group-Object -Property 'Name', 'FilePath' | ForEach-Object { $_.Group | Select-Object 'Name', 'FilePath' -First 1 } | Sort-Object 'Name' if ($ExcludeDirectChanges) { $reducedModules = @() foreach ($module in $impactedModules) { $existingModule = $changedModules | Where-Object { $_.Name -eq $module.Name } if ($null -eq $existingModule) { $reducedModules += $module } } $impactedModules = $reducedModules } Write-InformationEx "Get-BicepModuleChanged: Found $($impactedModules.Count) impacted modules." -ForegroundColor Green $impactedModules | Select-Object Name, FilePath | Format-Table | Out-String | Write-InformationEx return $impactedModules } Export-ModuleMember Get-BicepModuleChanged function Get-BicepModuleForPublish { Param( [Parameter(Mandatory=$true, Position=0, HelpMessage="PathSpec to grep Bicep modules to publish.")] [ValidateNotNullOrWhiteSpace()] [string] $PathSpec, [Parameter(Mandatory=$false, HelpMessage="Commit range to check for changes. If not supplied will return all files in pathSpec.")] [Alias("cr")] [string] $CommitRange, [Parameter(Mandatory=$false, HelpMessage="Exclude direct changes to files in pathSpec from being published.")] [Alias("ed")] [switch] $ExcludeDirectChanges ) $modulesToPublish = @() if ($CommitRange -eq "") { $files = Get-ChildItem -Recurse -Path $PathSpec $modulesToPublish = @() foreach ($file in $files) { $modulesToPublish += [BicepModule]::new($file) } } else { $modulesToPublish = Get-BicepModuleChanged $PathSpec $CommitRange -ExcludeDirectChanges:$ExcludeDirectChanges } Write-InformationEx "Get-BicepModuleForPublish: Found $($modulesToPublish.Count) files to publish." -ForegroundColor Green $modulesToPublish | Format-Table | Out-String | Write-InformationEx return $modulesToPublish } Export-ModuleMember Get-BicepModuleForPublish class BicepImport { [ValidateNotNullOrEmpty()][string]$Alias [ValidateNotNullOrEmpty()][string]$Name [string]$Version [string]$RegistryUrl [string]$LatestVersion [array]$FilePaths } function Get-BicepModuleImport([string] $pathSpec) { Write-InformationEx "Get-BicepModuleImport: $pathSpec" -ForegroundColor Green $moduleReferences = Get-ChildItem -recurse -Path $pathSpec | Select-String -pattern "\bmodule\b", "\bimport\b" | Select-Object $modules = @() for(($index = 0); $index -lt $moduleReferences.Count; $index++) { $moduleReference = $moduleReferences[$index] Format-Message "Reference $($moduleReference)" -Index $index | Write-DebugEx Format-Message "Line: '$($moduleReference.Line)'" -Index $index | Write-DebugEx $beginIndex = $moduleReference.Line.IndexOf("'")+1 $endIndex = $moduleReference.Line.IndexOf("'", $beginIndex) Format-Message "Begin: $($beginIndex) - End: $($endIndex)" -Index $index | Write-DebugEx $module = $moduleReference.Line.SubString($beginIndex, $endIndex - $beginIndex) Format-Message "Module: '$($module)'" -Index $index | Write-DebugEx $alias = '' $name = '' $version = '' if ($module.Contains(':')) { $moduleParts = $module.Split(':') $alias = $moduleParts[0] $name = $moduleParts[1] $version = $moduleParts[2] } elseif ($module.Contains('@')) { } elseif ($module.Contains('.bicep')) { $fileDir = Split-Path -Path $moduleReference.Path -Parent Format-Message "FileDir: $($fileDir)" -Index $index | Write-DebugEx $moduleName = Resolve-Path "$($fileDir)/$($module)" -Relative Format-Message "ModuleName: $($moduleName)" -Index $index | Write-DebugEx $alias = '.' $name = $moduleName $version = '' } $existingModule = $modules | Where-Object { $_.Alias -eq $alias -And $_.Name -eq $name} if ($null -eq $existingModule) { $modules += [BicepImport]@{ Alias = $alias Name = $name Version = $version FilePaths = @($moduleReference.Path) } } else { if ($existingModule.FilePaths -notcontains $moduleReference.Path) { $existingModule.FilePaths += $moduleReference.Path } } Write-DebugEx "-------------- END REFERENCE $($index) --------------" } Write-InformationEx "Get-BicepModuleImport: Found $($modules.Count) imports." -ForegroundColor Green $modules | Select-Object Alias, Name, Version, FilePaths | Format-Table | Out-String | Write-InformationEx return $modules } Export-ModuleMember Get-BicepModuleImport function Publish-BicepModule { Param( [Parameter(Mandatory = $true, Position = 0, HelpMessage = "PathSpec to grep Bicep modules to publish.")] [ValidateNotNullOrWhiteSpace()] [string] $PathSpec, [Parameter(Mandatory = $true, Position = 1, HelpMessage = "Registry name of Azure container registry to which to publish.")] [ValidateNotNullOrWhiteSpace()] [string] $RegistryName, [Parameter(Mandatory = $true, Position = 2, HelpMessage = "Version to be tagged to published modules.")] [ValidateNotNullOrWhiteSpace()] [string] $Version, [Parameter(Mandatory = $false, HelpMessage = "Commit range to check for changes.")] [Alias("cr")] [string] $CommitRange, [Parameter(Mandatory = $false, HelpMessage = "Exclude direct changes to files in pathSpec from being published.")] [Alias("ed")] [switch] $ExcludeDirectChanges ) $modulesToPublish = Get-BicepModuleForPublish $PathSpec $CommitRange -ExcludeDirectChanges:$ExcludeDirectChanges foreach ($module in $modulesToPublish) { Write-InformationEx "Publishing module $($module.Name) with version $($Version) to registry $($RegistryName)" az bicep publish --file $module.FilePath --target "br:$($RegistryName).azurecr.io/$($module.Name):$($Version)" --only-show-errors } } Export-ModuleMember Publish-BicepModule class FileToUpdate { [string]$Path [array]$Modules } <# .SYNOPSIS Updates the versions of the imports & modules from custom repositories to the latest version available in the registry. .DESCRIPTION Extracts from all files mathcing the pathspec, imports & module declarations that are using the custom repository syntax alias:modulename:version. Checks a newer version in the registry and updates the version in the files to the latest version available in the registry. Current support is limited to Azure Container Registry (ACR). .INPUTS None. You cannot pipe objects to Add-Extension. .OUTPUTS None. .EXAMPLE PS> Update-BicepModuleVersion '*.bicep' .LINK https://github.com/lukaferlez/Simpleverse.Bicep/blob/main/README.md #> function Update-BicepModuleVersion { [CmdletBinding(SupportsShouldProcess, ConfirmImpact= 'High')] Param( [Parameter(Mandatory = $true, Position = 0, HelpMessage = "PathSpec to grep Bicep modules to update.")] [ValidateNotNullOrWhiteSpace()] [string] $PathSpec, [Parameter(Mandatory = $false, HelpMessage = "Path to bicepconfig.json with defined registries.")] [Alias("b")] [string] $BicepConfigPath = 'bicepconfig.json', [Parameter(Mandatory = $false, HelpMessage = "Force update without confirmation.")] [Alias("f")] [switch] $Force ) $modules = Get-BicepModuleImport $PathSpec | Where-Object { $_.Alias -ne '.' } Write-InformationEx "Found $($modules.Count) modules." -ForegroundColor Green $modules | Select-Object Alias, Name, Version | Format-Table | Out-String | Write-InformationEx Write-InformationEx "Gathering latest versions from registry source." $bicepConfig = Get-Content $BicepConfigPath | ConvertFrom-Json -AsHashtable foreach ($module in $modules) { $aliasSplit = $module.Alias.Split("/") $module.registryUrl = $bicepConfig['moduleAliases'][$aliasSplit[0]][$aliasSplit[1]]['registry'] Write-InformationEx "Checking $($module.Alias) from registry $($module.registryUrl) for $($module.Name)" $module.LatestVersion = az acr repository show-tags --name $module.RegistryUrl.Replace('.azurecr.io', '') --repository $module.Name --top 1 --orderby time_desc | ConvertFrom-Json } $modulesForUpdate = $modules | Where-Object { $_.Version -ne $_.LatestVersion } if ($modulesForUpdate.Count -eq 0) { Write-InformationEx "All modules are up to date." -ForegroundColor Green return } Write-InformationEx "Modules to update." $modules | Where-Object { $_.Version -ne $_.LatestVersion } | Select-Object Alias, Name, Version, LatestVersion | Format-Table | Out-String | Write-InformationEx $filesToUpdate = @() foreach ($module in $modules | Where-Object { $_.Version -ne $_.LatestVersion }) { foreach ($filePath in $module.FilePaths) { $existingFilePath = $filesToUpdate | Where-Object { $_.Path -eq $filePath } if ($null -eq $existingFilePath) { $filesToUpdate += [FileToUpdate]@{ Path = $filePath Modules = @($module) } } else { $existingFilePath.Modules += $module } } } $filesToUpdate | Format-Table | Out-String | Write-InformationEx if ($Force -and -not $PSBoundParameters.ContainsKey('Confirm')) { $ConfirmPreference = 'None' } if ($PSCmdlet.ShouldProcess($filesToUpdate.Path, "Update")) { foreach ($fileToUpdate in $filesToUpdate) { $content = Get-Content $fileToUpdate.Path foreach ($module in $fileToUpdate.Modules) { $content = $content -replace "$($module.Alias):$($module.Name):$($module.Version)", "$($module.Alias):$($module.Name):$($module.LatestVersion)" } Set-Content -Path $fileToUpdate.Path -Value $content -Confirm:$false -WhatIf:$WhatIfPreference } Write-InformationEx "Updated $($filesToUpdate.Count) files." -ForegroundColor Green } } Export-ModuleMember Update-BicepModuleVersion |