TfPlanVisualizer.psm1
|
#requires -version 7.0 <# .SYNOPSIS TfPlanVisualizer module — renders Terraform plans as self-contained HTML reports. .DESCRIPTION Module root script. Dot-sources the internal parser (ConvertTo-PlanReportModel) at import time and exports New-TerraformPlanReport as the sole public cmdlet. #> $ErrorActionPreference = 'Stop' $script:LibPath = Join-Path -Path $PSScriptRoot -ChildPath 'lib' $script:ParserPath = Join-Path -Path $script:LibPath -ChildPath 'Parse-TerraformPlan.ps1' $script:TemplatePath = Join-Path -Path $script:LibPath -ChildPath 'template.html' if (-not (Test-Path -LiteralPath $script:ParserPath)) { throw "Parser script not found at '$script:ParserPath'." } # Dot-source at module load time so ConvertTo-PlanReportModel is available to # New-TerraformPlanReport below, but stays private (not exported). . $script:ParserPath function New-TerraformPlanReport { <# .SYNOPSIS Renders a `terraform show -json` plan as a single self-contained dark-themed HTML report and opens it in the default browser. .DESCRIPTION Local dev tool — no server, no build step. Supply either a saved binary Terraform plan file (`-PlanFile`, this cmdlet runs `terraform show -json` itself) or an already-exported `terraform show -json` file (`-PlanJson`). The plan is normalized by lib/Parse-TerraformPlan.ps1 and injected into lib/template.html in place of the __REPORT_DATA__ token. .PARAMETER PlanFile Path to a saved binary Terraform plan file (e.g. produced by `terraform plan -out=tfplan`). Mutually exclusive with -PlanJson. .PARAMETER PlanJson Path to already-exported `terraform show -json` output. Mutually exclusive with -PlanFile. .PARAMETER OutputPath Path to write the generated HTML report to. Defaults to ./terraform-plan-report.html (relative to the caller's current directory). .PARAMETER NoOpen Skip opening the generated report in the default browser. .EXAMPLE New-TerraformPlanReport -PlanFile ./tfplan .EXAMPLE terraform show -json ./tfplan > plan.json New-TerraformPlanReport -PlanJson ./plan.json -OutputPath ./report.html -NoOpen #> [CmdletBinding(DefaultParameterSetName = 'FromPlanFile')] param( [Parameter(Mandatory, ParameterSetName = 'FromPlanFile')] [ValidateNotNullOrEmpty()] [string] $PlanFile, [Parameter(Mandatory, ParameterSetName = 'FromPlanJson')] [ValidateNotNullOrEmpty()] [string] $PlanJson, [Parameter()] [string] $OutputPath = './terraform-plan-report.html', [Parameter()] [switch] $NoOpen ) $parserPath = $script:ParserPath $templatePath = $script:TemplatePath if (-not (Test-Path -LiteralPath $parserPath)) { Write-Error "Parser script not found at '$parserPath'." return } if (-not (Test-Path -LiteralPath $templatePath)) { Write-Error "HTML template not found at '$templatePath'." return } # --- Obtain raw `terraform show -json` output as a string ----------------- $rawJson = $null if ($PSCmdlet.ParameterSetName -eq 'FromPlanFile') { if (-not (Test-Path -LiteralPath $PlanFile)) { Write-Error "Plan file not found: '$PlanFile'." return } $terraformCmd = Get-Command -Name 'terraform' -ErrorAction SilentlyContinue if (-not $terraformCmd) { Write-Error "'terraform' was not found on PATH. Install Terraform, or export the plan yourself with 'terraform show -json <planfile> > plan.json' and pass it via -PlanJson instead." return } try { $resolvedPlanFile = (Resolve-Path -LiteralPath $PlanFile).Path $rawJson = & $terraformCmd.Source show -json $resolvedPlanFile 2>&1 if ($LASTEXITCODE -ne 0) { Write-Error "'terraform show -json' exited with code $LASTEXITCODE for plan file '$PlanFile':`n$rawJson" return } $rawJson = $rawJson -join [Environment]::NewLine } catch { Write-Error "Failed to run 'terraform show -json' against '$PlanFile': $($_.Exception.Message)" return } } else { if (-not (Test-Path -LiteralPath $PlanJson)) { Write-Error "Plan JSON file not found: '$PlanJson'." return } try { $rawJson = Get-Content -LiteralPath $PlanJson -Raw } catch { Write-Error "Failed to read plan JSON from '$PlanJson': $($_.Exception.Message)" return } } # --- Parse and normalize ---------------------------------------------------- $planObject = $null try { $planObject = $rawJson | ConvertFrom-Json -Depth 100 } catch { Write-Error "Failed to parse plan JSON: $($_.Exception.Message)" return } $reportModel = $null try { $reportModel = ConvertTo-PlanReportModel -PlanJson $planObject } catch { Write-Error "Failed to normalize plan JSON into a report model: $($_.Exception.Message)" return } $reportModelJson = $null try { $reportModelJson = $reportModel | ConvertTo-Json -Depth 20 -Compress } catch { Write-Error "Failed to serialize report model: $($_.Exception.Message)" return } # --- Inject into template and write output ---------------------------------- $templateContent = $null try { $templateContent = Get-Content -LiteralPath $templatePath -Raw } catch { Write-Error "Failed to read template from '$templatePath': $($_.Exception.Message)" return } if ($templateContent -notmatch [regex]::Escape('__REPORT_DATA__')) { Write-Error "Template at '$templatePath' does not contain the expected '__REPORT_DATA__' placeholder." return } $reportHtml = $templateContent.Replace('__REPORT_DATA__', $reportModelJson) try { Set-Content -LiteralPath $OutputPath -Value $reportHtml -NoNewline -Encoding utf8 } catch { Write-Error "Failed to write report to '$OutputPath': $($_.Exception.Message)" return } $resolvedOutput = (Resolve-Path -LiteralPath $OutputPath).Path Write-Host "Report written to: $resolvedOutput" if (-not $NoOpen) { try { if ($IsMacOS) { & open $resolvedOutput } elseif ($IsWindows) { Start-Process $resolvedOutput } elseif ($IsLinux) { & xdg-open $resolvedOutput } else { Start-Process $resolvedOutput } } catch { Write-Error "Report was written successfully but could not be opened automatically: $($_.Exception.Message)" } } } Export-ModuleMember -Function New-TerraformPlanReport |