src/public/Configuration/Compare-AitherConfig.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Compare two configuration files or sections .DESCRIPTION Shows differences between two configurations, highlighting added, removed, and changed values. .PARAMETER Config1 First configuration file or section name .PARAMETER Config2 Second configuration file or section name .PARAMETER Section Compare only specific section .PARAMETER ShowEqual Show values that are the same .PARAMETER Export Export diff to file .EXAMPLE Compare-AitherConfig -Config1 'config.psd1' -Config2 'config.local.psd1' .EXAMPLE Compare-AitherConfig -Config1 'config.psd1' -Config2 'config.production.psd1' -Section Automation .NOTES Useful for comparing environments or tracking configuration changes. #> function Compare-AitherConfig { [CmdletBinding()] param( [Parameter(Mandatory=$false)] [string]$Config1, [Parameter(Mandatory=$false)] [string]$Config2, [Parameter()] [string]$Section, [Parameter()] [switch]$ShowEqual, [Parameter()] [string]$Export ) begin { $moduleRoot = Get-AitherModuleRoot function Get-ConfigFromPath { param([string]$Path) if (Test-Path $Path) { $content = Get-Content -Path $Path -Raw $scriptBlock = [scriptblock]::Create($content) return & $scriptBlock } elseif ($Path -match '^config\.') { # Try as config file name $fullPath = Join-Path $moduleRoot $Path if (Test-Path $fullPath) { $content = Get-Content -Path $fullPath -Raw $scriptBlock = [scriptblock]::Create($content) return & $scriptBlock } } else { # Try as section name return Get-AitherConfigs -Section $Path } throw "Could not load configuration: $Path" } function Compare-Hashtables { param( [hashtable]$Left, [hashtable]$Right, [string]$Path = '' ) $differences = @() # Get all keys $allKeys = ($Left.Keys + $Right.Keys) | Select-Object -Unique foreach ($key in $allKeys) { $currentPath = if ($Path) { "$Path.$key" } else { $key } $leftValue = $Left[$key] $rightValue = $Right[$key] # Key only in left if ($Left.ContainsKey($key) -and -not $Right.ContainsKey($key)) { $differences += [PSCustomObject]@{ Path = $currentPath Type = 'Removed' Left = $leftValue Right = $null } } # Key only in right elseif (-not $Left.ContainsKey($key) -and $Right.ContainsKey($key)) { $differences += [PSCustomObject]@{ Path = $currentPath Type = 'Added' Left = $null Right = $rightValue } } # Both have key - compare values else { if ($leftValue -is [hashtable] -and $rightValue -is [hashtable]) { $differences += Compare-Hashtables -Left $leftValue -Right $rightValue -Path $currentPath } elseif ($leftValue -ne $rightValue) { $differences += [PSCustomObject]@{ Path = $currentPath Type = 'Changed' Left = $leftValue Right = $rightValue } } elseif ($ShowEqual) { $differences += [PSCustomObject]@{ Path = $currentPath Type = 'Equal' Left = $leftValue Right = $rightValue } } } } return $differences } } process { # Don't execute if parameters not provided (during module loading) if (-not $Config1 -or -not $Config2) { return } try { # Load configurations $config1Obj = Get-ConfigFromPath -Path $Config1 $config2Obj = Get-ConfigFromPath -Path $Config2 # Filter by section if specified if ($Section) { if ($config1Obj.ContainsKey($Section)) { $config1Obj = $config1Obj[$Section] } else { throw "Section '$Section' not found in Config1" } if ($config2Obj.ContainsKey($Section)) { $config2Obj = $config2Obj[$Section] } else { throw "Section '$Section' not found in Config2" } } # Compare $differences = Compare-Hashtables -Left $config1Obj -Right $config2Obj # Display results Write-AitherLog -Level Information -Message "=== Configuration Comparison ===" -Source 'Compare-AitherConfig' Write-AitherLog -Level Information -Message "Config1: $Config1" -Source 'Compare-AitherConfig' Write-AitherLog -Level Information -Message "Config2: $Config2" -Source 'Compare-AitherConfig' if ($differences.Count -eq 0) { Write-AitherLog -Level Information -Message "No differences found." -Source 'Compare-AitherConfig' } else { $added = ($differences | Where-Object { $_.Type -eq 'Added' }).Count $removed = ($differences | Where-Object { $_.Type -eq 'Removed' }).Count $changed = ($differences | Where-Object { $_.Type -eq 'Changed' }).Count Write-AitherLog -Level Information -Message "Summary:" -Source 'Compare-AitherConfig' Write-AitherLog -Level Information -Message " Added: $added" -Source 'Compare-AitherConfig' Write-AitherLog -Level Information -Message " Removed: $removed" -Source 'Compare-AitherConfig' Write-AitherLog -Level Information -Message " Changed: $changed" -Source 'Compare-AitherConfig' # Show differences foreach ($diff in $differences) { $level = switch ($diff.Type) { 'Added' { 'Information' } 'Removed' { 'Warning' } 'Changed' { 'Warning' } 'Equal' { 'Information' } default { 'Information' } } Write-AitherLog -Level $level -Message "[$($diff.Type)] $($diff.Path)" -Source 'Compare-AitherConfig' if ($diff.Type -eq 'Changed') { Write-AitherLog -Level Warning -Message " -: $($diff.Left)" -Source 'Compare-AitherConfig' Write-AitherLog -Level Information -Message " +: $($diff.Right)" -Source 'Compare-AitherConfig' } elseif ($diff.Type -eq 'Added') { Write-AitherLog -Level Information -Message " +: $($diff.Right)" -Source 'Compare-AitherConfig' } elseif ($diff.Type -eq 'Removed') { Write-AitherLog -Level Warning -Message " -: $($diff.Left)" -Source 'Compare-AitherConfig' } } } # Export if requested if ($Export) { $differences | ConvertTo-Json -Depth 10 | Set-Content -Path $Export Write-AitherLog -Level Information -Message "Differences exported to: $Export" -Source 'Compare-AitherConfig' } return $differences } catch { Write-AitherLog -Level Error -Message "Failed to compare configurations: $_" -Source 'Compare-AitherConfig' -Exception $_ throw } } } |