Private/Create-htmlReport.ps1
function Generate-K8sHTMLReport { param ( [string]$outputPath, [string]$version = "v0.0.1", [string]$SubscriptionId, [string]$ResourceGroup, [string]$ClusterName, [switch]$aks, [switch]$ExcludeNamespaces, [object]$KubeData ) function ConvertToCollapsible { param( [string]$Id, [string]$defaultText, [string]$content ) @" <div class="collapsible-container" id='$Id'> <details style='margin:10px 0;'> <summary style='font-size:16px; cursor:pointer; color:#0071FF; font-weight:bold;'>$defaultText</summary> <div style='padding-top: 15px;'> $content </div> </details> </div> "@ } # Mapping of custom check sections to navigation categories $sectionToNavMap = @{ "Nodes" = "Nodes" "Namespaces" = "Namespaces" "Workloads" = "Workloads" "Pods" = "Pods" "Jobs" = "Jobs" "Networking" = "Networking" "Storage" = "Storage" "Configuration" = "Configuration Hygiene" "Security" = "Security" "Kubernetes Events" = "Kubernetes Events" } if (Test-Path $outputPath) { Remove-Item $outputPath -Force } # Path to report-scripts.js and report-styles.css in the module directory $jsPath = Join-Path $PSScriptRoot "html/report-scripts.js" $cssPath = Join-Path $PSScriptRoot "html/report-styles.css" # Read the JavaScript content if (Test-Path $jsPath) { $jsContent = Get-Content -Path $jsPath -Raw } else { $jsContent = "// Error: report-scripts.js not found at $jsPath" Write-Warning "report-scripts.js not found at $jsPath. HTML features may not work." } # Read CSS content $cssPath = Join-Path $PSScriptRoot "html/report-styles.css" if (-not (Test-Path $cssPath)) { Write-Warning "CSS file not found at $cssPath. Using fallback styles." $cssContent = @" body { font-family: Arial, sans-serif; margin: 0; padding: 0; background: #f5f5f5; } .header { background: #0071FF; color: white; padding: 10px; } .tabs li { display: inline-block; padding: 10px 20px; cursor: pointer; } .tabs li.active { background: #005BB5; } .table-container { margin: 20px 0; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } .tooltip { position: relative; display: inline-block; } .tooltip .tooltip-text { visibility: hidden; background: #333; color: white; padding: 5px; position: absolute; z-index: 1; } .tooltip:hover .tooltip-text { visibility: visible; } "@ } else { $cssContent = Get-Content -Path $cssPath -Raw } Write-Host "`n[🌐 Cluster Summary]" -ForegroundColor Cyan Write-Host -NoNewline "`n🤖 Fetching Cluster Information..." -ForegroundColor Yellow $clusterSummaryRaw = Show-ClusterSummary -Html -KubeData:$KubeData *>&1 $apiHealthHtml = Show-ApiServerHealth -html Write-Host "`r🤖 Cluster Information fetched. " -ForegroundColor Green if ($aks) { Write-Host -NoNewline "`n🤖 Running AKS Best Practices Checklist..." -ForegroundColor Cyan $aksBestPractices = Invoke-AKSBestPractices -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -ClusterName $ClusterName -Html -KubeData:$KubeData Write-Host "`r🤖 AKS Check Results fetched. " -ForegroundColor Green $aksPass = $aksBestPractices.Passed $aksFail = $aksBestPractices.Failed $aksTotal = $aksBestPractices.Total $aksScore = $aksBestPractices.Score $aksRating = $aksBestPractices.Rating $aksReportData = $aksBestPractices.Data $collapsibleAKSHtml = ConvertToCollapsible -Id "aksSummary" -defaultText "Show Findings" -content $aksReportData $ratingColorClass = switch ($aksRating) { "A" { "normal" } "B" { "warning" } "C" { "warning" } "D" { "critical" } "F" { "critical" } default { "unknown" } } # Use ScoreColor directly for the score box (hex color for inline style) $heroRatingHtml = @" <h2>AKS Best Practices Summary</h2> <div class="hero-metrics"> <div class="metric-card normal">✅ Passed: <strong>$aksPass</strong></div> <div class="metric-card critical">❌ Failed: <strong>$aksFail</strong></div> <div class="metric-card default">📊 Total Checks: <strong>$aksTotal</strong></div> <div class="metric-card default">🎯 Score: <strong>$aksScore%</strong></div> <div class="metric-card $ratingColorClass">⭐ Rating: <strong>$aksRating</strong></div> </div> "@ $aksHealthCheck = @" <div class="container"> <h1 id="aks">AKS Best Practices</h1> $heroRatingHtml <h2 id="aksFindings">AKS Best Practices Results</h2> <div class="table-container"> $collapsibleAKSHtml </div> </div> "@ $aksMenuItem = @" <li class="nav-item"><a href="#aks"><span class="material-icons">verified</span> AKS Best Practices</a></li> "@ } $checks = @( @{ Id = "allChecks"; Cmd = { Invoke-yamlChecks -Html -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData } } ) $customNavItems = @{} $checkStatusList = @() $hasCustomChecks = $false foreach ($check in $checks) { $html = & $check.Cmd if (-not $html) { $html = "<p>No data available for $($check.Id).</p>" } if ($check.Id -eq 'allChecks' -and $html -is [hashtable]) { $allChecksBySection = $html.HtmlBySection $checkStatusList += $html.StatusList $checkScoreList += $html.ScoreList $knownSections = $sectionToNavMap.Keys foreach ($section in $allChecksBySection.Keys) { # --- build your checksInSection exactly as you had it --- $sectionHtml = $allChecksBySection[$section] $checkIds = [regex]::Matches($sectionHtml, "<h2 id='([^']+)'") | ForEach-Object { $_.Groups[1].Value } $checkNames = [regex]::Matches($sectionHtml, "<h2 id='[^']+'>\s*[^-]+-\s*([^<]+)") | ForEach-Object { $_.Groups[1].Value.Trim() } $checksInSection = for ($i = 0; $i -lt [Math]::Min($checkIds.Count, $checkNames.Count); $i++) { @{ Id = $checkIds[$i]; Name = $checkNames[$i] } } # pick tab by mapping or fallback if ($knownSections -contains $section) { $navSection = $sectionToNavMap[$section] } else { $navSection = 'Custom Checks' } # accumulate nav‑items if (-not $customNavItems[$navSection]) { $customNavItems[$navSection] = @() } $customNavItems[$navSection] += $checksInSection # store the per‑section HTML $varName = "collapsible$($section -replace '[^\w]','')Html" if (Get-Variable -Name $varName -Scope Script -ErrorAction SilentlyContinue) { Set-Variable -Name $varName -Value ((Get-Variable $varName -ValueOnly) + "`n" + $sectionHtml) } else { Set-Variable -Name $varName -Value $sectionHtml } } # now build the Custom‑Checks tab only from that one bucket if ($customNavItems['Custom Checks'] -and $customNavItems['Custom Checks'].Count) { $snippets = foreach ($chk in $customNavItems['Custom Checks']) { $htmlVar = "collapsible$($chk.Id -replace '[^\w]','')Html" $s = Get-Variable -Name $htmlVar -ValueOnly -ErrorAction SilentlyContinue if ($s -match '<tr>.*?<td>.*?</td>.*?</tr>') { $s } } if ($snippets.Count) { $collapsibleCustomChecksHtml = $snippets -join "`n`n" $hasCustomChecks = $true } } continue } $pre = "" if ($html -match '^\s*<p>.*?</p>') { $pre = $matches[0] $html = $html -replace [regex]::Escape($pre), "" } elseif ($html -match '^\s*[^<]+$') { $lines = $html -split "`n", 2 $pre = "<p>$($lines[0].Trim())</p>" $html = if ($lines.Count -gt 1) { $lines[1] } else { "" } } else { $pre = "<p>⚠️ $($check.Id) Report</p>" } $hasIssues = $html -match '<tr>.*?<td>.*?</td>.*?</tr>' -and $html -notmatch 'No data available' $content = if ($noFindings) { "$pre`n" } else { "$pre`n" + (ConvertToCollapsible -Id $check.Id -defaultText $defaultText -content $html) } Set-Variable -Name ("collapsible" + $check.Id + "Html") -Value $content } $clusterSummaryText = $clusterSummaryRaw -join "`n" function Extract-Metric($label, $data) { if ($data -match "$label\s*:\s*([\d]+)") { [int]$matches[1] } else { "0" } } $clusterName = "Unknown" $k8sVersion = "Unknown" $clusterScore = Get-ClusterHealthScore -Checks $checkScoreList $scoreColor = if ($clusterScore -ge 80) { "#4CAF50" } elseif ($clusterScore -ge 50) { "#FF9800" } else { "#F44336" } $scoreBarHtml = @" <div class="score-container"> <p>Score: <strong>$clusterScore / 100</strong></p> <div class="progress-bar" style="--cluster-score: $clusterScore%; --score-color: $scoreColor;"> <div class="progress"> <span class="progress-text">$clusterScore%</span> </div> </div> <p style="margin-top:10px; font-size:16px;">This score is calculated from key checks across nodes, workloads, security, and configuration best practices. A higher score means fewer issues and better adherence to Kubernetes standards.</p> </div> "@ $totalChecks = $checkStatusList.Count $passedChecks = ($checkStatusList | Where-Object { $_.Status -eq 'Passed' }).Count if ($aks) { $totalChecks += $aksTotal $passedChecks += $aksPass } $issuesChecks = $totalChecks - $passedChecks $passedPercent = if ($totalChecks -gt 0) { [math]::Round(($passedChecks / $totalChecks) * 100, 2) } else { 0 } $donutStroke = $scoreColor $pieChartHtml = @" <svg class="pie-chart donut" width="120" height="120" viewBox="0 0 36 36" style="--percent: $passedPercent;"> <circle class="donut-ring" cx="18" cy="18" r="15.9155" stroke="#ECEFF1" stroke-width="4" fill="transparent"/> <circle class="donut-segment" cx="18" cy="18" r="15.9155" stroke="$donutStroke" stroke-width="4" stroke-dasharray="$passedPercent, 100" stroke-dashoffset="25" stroke-linecap="round" fill="transparent"/> <text x="18" y="20.5" text-anchor="middle" dominant-baseline="middle" font-size="8" fill="#37474F" transform="rotate(90 18 18)"> $passedChecks/$totalChecks </text> <circle id="pulseDot" r="0.6" fill="$donutStroke"/> </svg> "@ for ($i = 0; $i -lt $clusterSummaryRaw.Count; $i++) { $line = [string]$clusterSummaryRaw[$i] -replace "`r", "" -replace "`n", "" if ($line -match "Cluster Name\s*$") { $clusterName = [string]$clusterSummaryRaw[$i + 2] -replace "`r", "" -replace "`n", "" } if ($line -match "Kubernetes Version\s*$") { $k8sVersion = [string]$clusterSummaryRaw[$i + 2] -replace "`r", "" -replace "`n", "" } } $compatibilityCheck = if ($clusterSummaryText -match "⚠️\s+(Cluster is running an outdated version:[^\n]+)") { $matches[1].Trim(); $compatibilityClass = "warning" } elseif ($clusterSummaryText -match "✅ Cluster is up to date \((.*?)\)") { "✅ Cluster is up to date ($matches[1])"; $compatibilityClass = "healthy" } else { "Unknown"; $compatibilityClass = "unknown" } $totalNodes = Extract-Metric "🚀 Nodes" $clusterSummaryText $healthyNodes = Extract-Metric "🟩 Healthy" $clusterSummaryText $issueNodes = Extract-Metric "🟥 Issues" $clusterSummaryText $totalPods = Extract-Metric "📦 Pods" $clusterSummaryText $runningPods = Extract-Metric "🟩 Running" $clusterSummaryText $failedPods = Extract-Metric "🟥 Failed" $clusterSummaryText $totalRestarts = Extract-Metric "🔄 Restarts" $clusterSummaryText $warnings = Extract-Metric "🟨 Warnings" $clusterSummaryText $critical = Extract-Metric "🟥 Critical" $clusterSummaryText $pendingPods = Extract-Metric "⏳ Pending Pods" $clusterSummaryText $stuckPods = Extract-Metric "⚠️ Stuck Pods" $clusterSummaryText $jobFailures = Extract-Metric "📉 Job Failures" $clusterSummaryText $eventWarnings = Extract-Metric "⚠️ Warnings" $clusterSummaryText $eventErrors = Extract-Metric "❌ Errors" $clusterSummaryText $podAvg = if ($clusterSummaryText -match "📊 Pod Distribution: Avg: ([\d.]+)") { $matches[1] } else { "0" } $podMax = if ($clusterSummaryText -match "Max: ([\d.]+)") { $matches[1] } else { "0" } $podMin = if ($clusterSummaryText -match "Min: ([\d.]+)") { $matches[1] } else { "0" } $podTotalNodes = if ($clusterSummaryText -match "Total Nodes: ([\d]+)") { $matches[1] } else { "0" } $cpuUsage = if ($clusterSummaryText -match "🖥 CPU Usage:\s*([\d.]+)%") { [double]$matches[1] } else { 0 } $cpuStatus = if ($clusterSummaryText -match "🖥 CPU Usage:.*(🟩 Normal|🟡 Warning|🔴 Critical)") { $matches[1] } else { "Unknown" } $memUsage = if ($clusterSummaryText -match "💾 Memory Usage:\s*([\d.]+)%") { [double]$matches[1] } else { 0 } $memStatus = if ($clusterSummaryText -match "💾 Memory Usage:.*(🟩 Normal|🟡 Warning|🔴 Critical)") { $matches[1] } else { "Unknown" } $today = (Get-Date).ToUniversalTime().ToString("MMMM dd, yyyy HH:mm:ss 'UTC'") $year = (Get-Date).ToUniversalTime().ToString("yyyy") $thresholds = Get-KubeBuddyThresholds -Silent $excludedNamespaces = Get-ExcludedNamespaces -Silent $errorClass = if ($eventErrors -ge $thresholds.event_errors_critical) { "critical" } elseif ($eventErrors -ge $thresholds.event_errors_warning) { "warning" } else { "normal" } $warningClass = if ($eventWarnings -ge $thresholds.event_warnings_critical) { "critical" } elseif ($eventWarnings -ge $thresholds.event_warnings_warning) { "warning" } else { "normal" } $cpuClass = if ($cpuUsage -ge $thresholds.cpu_critical) { "critical" } elseif ($cpuUsage -ge $thresholds.cpu_warning) { "warning" } else { "normal" } $memClass = if ($memUsage -ge [double]$thresholds.mem_critical) { "critical" } elseif ($memUsage -ge [double]$thresholds.mem_warning) { "warning" } else { "normal" } if ($ExcludeNamespaces) { $excludedList = ($excludedNamespaces | ForEach-Object { "<span class='excluded-ns'>$_</span>" }) -join " • " $excludedNamespacesHtml = @" <h2>Excluded Namespaces <span class="tooltip"><span class="info-icon">i</span><span class="tooltip-text">These namespaces are excluded from analysis and reporting.</span></span> </h2> <p>$excludedList</p> "@ } else { $excludedNamespacesHtml = "" } $htmlTemplate = @" <!DOCTYPE html> <html lang='en'> <head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <title>Kubernetes Cluster Report</title> <link rel='icon' href='https://raw.githubusercontent.com/KubeDeckio/KubeBuddy/refs/heads/main/docs/assets/images/favicon.ico' type='image/x-icon'> <link href='https://fonts.googleapis.com/icon?family=Material+Icons' rel='stylesheet'> <style> $cssContent </style> </head> <body> <div class="wrapper"> <div class="main-content"> <div class="header"> <div class="header-inner"> <div class="header-top"> <div> <span>Kubernetes Cluster Report: $ClusterName</span> <br> <span style="font-size: 12px;"> Powered by <img src="https://raw.githubusercontent.com/KubeDeckio/KubeBuddy/refs/heads/main/images/reportheader%20(2).png" alt="KubeBuddy Logo" style="height: 70px; vertical-align: middle;"> </span> </div> <div style="text-align: right; font-size: 13px; line-height: 1.4;"> <div>Generated on: <strong>$today</strong></div> <div> Created by <a href="https://kubedeck.io" target="_blank" style="color: #ffffff; text-decoration: underline;"> 🌐 KubeDeck.io </a> </div> <div id="printContainer" style="margin-top: 4px;"> <button id="savePdfBtn">📄 Save as PDF</button> </div> </div> </div> <ul class="tabs"> <li class="tab active" data-tab="summary" data-tooltip="Summary">Summary</li> <li class="tab" data-tab="nodes" data-tooltip="Nodes">Nodes</li> <li class="tab" data-tab="namespaces" data-tooltip="Namespaces">Namespaces</li> <li class="tab" data-tab="workloads" data-tooltip="Workloads">Workloads</li> <li class="tab" data-tab="pods" data-tooltip="Pods">Pods</li> <li class="tab" data-tab="jobs" data-tooltip="Jobs">Jobs</li> <li class="tab" data-tab="networking" data-tooltip="Networking">Networking</li> <li class="tab" data-tab="storage" data-tooltip="Storage">Storage</li> <li class="tab" data-tab="configuration" data-tooltip="Configuration">Configuration</li> <li class="tab" data-tab="security" data-tooltip="Security">Security</li> <li class="tab" data-tab="events" data-tooltip="Kubernetes Events">Kubernetes Events</li> $(if ($hasCustomChecks) { '<li class="tab" data-tab="customChecks" data-tooltip="Custom Checks">Custom Checks</li>' }) $(if ($aks) { '<li class="tab" data-tab="aks" data-tooltip="AKS Best Practices">AKS Best Practices</li>' }) </ul> </div> </div> <div id="navDrawer" class="nav-drawer"> <div class="nav-header"> <h3>Menu</h3> <button id="navClose" class="nav-close">×</button> </div> <ul class="nav-items"></ul> </div> <div id="navScrim" class="nav-scrim"></div> <button id="menuFab">☰</button> <div class="tab-content active" id="summary"> <div class="container"> <h1 id="Health">Cluster Overview</h1> <div class="cluster-health"> <div class="health-score"> <h2>Cluster Health Score</h2> $scoreBarHtml </div> <div class="api-summary"> <h2>API Server Health</h2> $apiHealthHtml </div> <div class="health-pie centered-donut"> <h2>Passed / Failed Checks</h2> $pieChartHtml </div> </div> </div> <div class="container"> <h1 id="summary">Cluster Summary</h1> <p><strong>Cluster Name:</strong> $ClusterName</p> <p><strong>Kubernetes Version:</strong> $k8sVersion</p> <div class="compatibility $compatibilityClass"><strong>$compatibilityCheck</strong></div> <h2>Cluster Metrics Summary <span class="tooltip"><span class="info-icon">i</span><span class="tooltip-text">Summary of metrics including node and pod counts, warnings, and issues.</span></span></h2> <table> <tr><td>🚀 Nodes: $totalNodes</td><td>🟩 Healthy: $healthyNodes</td><td>🟥 Issues: $issueNodes</td></tr> <tr><td>📦 Pods: $totalPods</td><td>🟩 Running: $runningPods</td><td>🟥 Failed: $failedPods</td></tr> <tr><td>🔄 Restarts: $totalRestarts</td><td>🟨 Warnings: $warnings</td><td>🟥 Critical: $critical</td></tr> <tr><td>⏳ Pending Pods: $pendingPods</td><td>🟡 Waiting: $pendingPods</td></tr> <tr><td>⚠️ Stuck Pods: $stuckPods</td><td>❌ Stuck: $stuckPods</td></tr> <tr><td>📉 Job Failures: $jobFailures</td><td>🔴 Failed: $jobFailures</td></tr> </table> <h2>Pod Distribution <span class="tooltip"><span class="info-icon">i</span><span class="tooltip-text">Average, min, and max pods per node and total node count.</span></span></h2> <table> <tr><td>Avg: <strong>$podAvg</strong></td><td>Max: <strong>$podMax</strong></td><td>Min: <strong>$podMin</strong></td><td>Total Nodes: <strong>$podTotalNodes</strong></td></tr> </table> <h2>Resource Usage <span class="tooltip"><span class="info-icon">i</span><span class="tooltip-text">Cluster-wide CPU and memory usage.</span></span></h2> <div class="hero-metrics"> <div class="metric-card $cpuClass">🖥 CPU: <strong>$cpuUsage%</strong><br><span>$cpuStatus</span></div> <div class="metric-card $memClass">💾 Memory: <strong>$memUsage%</strong><br><span>$memStatus</span></div> </div> <h2>Cluster Events <span class="tooltip"><span class="info-icon">i</span><span class="tooltip-text">Summary of recent warning and error events.</span></span></h2> <div class="hero-metrics"> <div class="metric-card $errorClass">❌ Errors: <strong>$eventErrors</strong></div> <div class="metric-card $warningClass">⚠️ Warnings: <strong>$eventWarnings</strong></div> </div> $excludedNamespacesHtml </div> </div> <div class="tab-content" id="nodes"> <div class="container"> <h1>Node Conditions & Resources</h1> <div class="table-container">$collapsibleNodesHtml</div> </div> </div> <div class="tab-content" id="namespaces"> <div class="container"> <h1>Namespaces</h1> <div class="table-container">$collapsibleNamespacesHtml</div> </div> </div> <div class="tab-content" id="workloads"> <div class="container"> <h1>Workloads</h1> <div class="table-container">$collapsibleWorkloadsHtml</div> </div> </div> <div class="tab-content" id="pods"> <div class="container"> <h1>Pods</h1> <div class="table-container">$collapsiblePodsHtml</div> </div> </div> <div class="tab-content" id="jobs"> <div class="container"> <h1>Jobs</h1> <div class="table-container">$collapsibleJobsHtml</div> </div> </div> <div class="tab-content" id="networking"> <div class="container"> <h1>Networking</h1> <div class="table-container">$collapsibleNetworkingHtml</div> </div> </div> <div class="tab-content" id="storage"> <div class="container"> <h1>Storage</h1> <div class="table-container">$collapsibleStorageHtml</div> </div> </div> <div class="tab-content" id="configuration"> <div class="container"> <h1>Configuration Hygiene</h1> <div class="table-container">$collapsibleConfigurationHygieneHtml</div> </div> </div> <div class="tab-content" id="security"> <div class="container"> <h1>Security</h1> <div class="table-container">$collapsibleSecurityHtml</div> </div> </div> <div class="tab-content" id="events"> <div class="container"> <h1>Kubernetes Warning Events</h1> <div class="table-container">$collapsibleKubernetesEventsHtml</div> </div> </div> $(if ($hasCustomChecks) { @" <div class="tab-content" id="customChecks"> <div class="container"> <h1>Custom Checks</h1> <div class="table-container">$collapsibleCustomChecksHtml</div> </div> </div> "@ }) $(if ($aks) { @" <div class="tab-content" id="aks"> $aksHealthCheck </div> "@ }) </div> <footer class="footer"> <img src="https://raw.githubusercontent.com/KubeDeckio/KubeBuddy/refs/heads/main/images/reportheader%20(2).png" alt="KubeBuddy Logo" class="logo"> <p><strong>Report generated by KubeBuddy $version</strong> on $today</p> <p>© $year KubeBuddy | <a href="https://kubedeck.io" target="_blank">KubeDeck.io</a></p> <p><em>This report is a snapshot of the cluster state at the time of generation. Always verify configurations before making critical decisions.</em></p> </footer> </div> <a href="#top" id="backToTop">Back to Top</a> <script> $jsContent </script> </body> </html> "@ $htmlTemplate | Set-Content $outputPath } |