Private/Show-AACAksView.ps1
|
function Show-AACAksView { <# .SYNOPSIS Renders Invoke-AACAksAssessment's result as a Spectre.Console view. .DESCRIPTION The scope; tiles (clusters, nodes, WAF score, findings by severity, out of support, policy violations, PSRule failures); the clusters with their version and support, WAF score per pillar and findings; the pillars as a bar chart; the High and Medium findings with what to do; policy violations by namespace; and, for up to -Detail clusters, each one in detail - node pools, versions and the failed checks. Wrap the call in Invoke-AACPagedOutput to page it. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable] $Assessment, [System.Collections.IDictionary] $Scope, [int] $Detail = 3 ) $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) } $glyph = Get-AACGlyph $stats = $Assessment.Stats $policy = $Assessment.Policy $color = @{ High = 'red1'; Medium = 'orange1'; Low = 'deepskyblue1'; Info = 'grey62' } $scoreColor = { param($Score) if ($null -eq $Score) { 'grey50' } elseif ($Score -ge 80) { 'green3' } elseif ($Score -ge 60) { 'orange1' } else { 'red1' } } $score = { param($Score) if ($null -eq $Score) { '[grey50]-[/]' } else { "[$(& $scoreColor $Score)]$Score%[/]" } } $newTable = { param([string] $TitleText, [string[]] $Headers, [string[]] $Right = @()) $t = [Spectre.Console.Table]::new(); $t.Border = [Spectre.Console.TableBorder]::Rounded; $t.BorderStyle = [Spectre.Console.Style]::Parse('deepskyblue3_1'); $t.Expand = $true if ($TitleText) { $t.Title = [Spectre.Console.TableTitle]::new($TitleText) } foreach ($header in $Headers) { $column = [Spectre.Console.TableColumn]::new("[grey62]$header[/]"); if ($header -in $Right) { $column.Alignment = [Spectre.Console.Justify]::Right }; $t.AddColumn($column) | Out-Null } $t } $addRow = { param($Table, [string[]] $Cells) [Spectre.Console.TableExtensions]::AddRow($Table, [Spectre.Console.Rendering.IRenderable[]]@($Cells | ForEach-Object { [Spectre.Console.Markup]::new($_) })) | Out-Null } $facts = [System.Collections.Generic.List[string]]::new() if ($script:AACSession) { $facts.Add("[white]$(& $escape $script:AACSession.Account)[/]") } if ($Scope) { foreach ($key in $Scope.Keys) { $facts.Add("$(& $escape $key): $(& $escape $Scope[$key])") } } $facts.Add((Get-Date).ToString('d MMM yyyy HH:mm')) Write-AACMarkup "[grey58]$($facts -join " $($glyph.Dot) ")[/]" foreach ($line in @($Assessment['Notices'])) { Write-AACMarkup "[orange1]$(& $escape $line)[/]" } [Spectre.Console.AnsiConsole]::WriteLine() Show-AACTileRow -Tile @( @{ Value = '{0:N0}' -f $stats.Clusters; Caption = "clusters, $('{0:N0}' -f $stats.Nodes) nodes"; Color = 'deepskyblue1' } @{ Value = $(if ($null -ne $stats.WafScore) { "$($stats.WafScore)%" } else { '-' }); Caption = 'WAF checks passed'; Color = (& $scoreColor $stats.WafScore) } @{ Value = '{0:N0}' -f $stats.High; Caption = 'high findings'; Color = $(if ($stats.High) { 'red1' } else { 'green3' }) } @{ Value = '{0:N0}' -f $stats.Medium; Caption = 'medium findings'; Color = $(if ($stats.Medium) { 'orange1' } else { 'green3' }) } @{ Value = '{0:N0}' -f $stats.OutOfSupport; Caption = 'out of support'; Color = $(if ($stats.OutOfSupport) { 'red1' } else { 'green3' }) } @{ Value = '{0:N0}' -f $stats.PSRuleFailed; Caption = 'PSRule failed'; Color = $(if ($stats.PSRuleFailed) { 'orange1' } else { 'green3' }) } if ($policy) { @{ Value = '{0:N0}' -f $policy.Stats.Violations; Caption = 'policy violations'; Color = $(if ($policy.Stats.Violations) { 'orange1' } else { 'green3' }) } } ) [Spectre.Console.AnsiConsole]::WriteLine() $table = & $newTable "[bold]$($glyph.Bullet) Clusters[/] [grey58]$($glyph.Dot) most at risk first[/]" @('Cluster', 'Version', 'Nodes', 'Network / API', 'Rel.', 'Sec.', 'Cost', 'Ops', 'Perf.', 'Findings') @('Nodes') foreach ($c in $Assessment.Clusters) { $counts = @(foreach ($level in 'High', 'Medium', 'Low') { if ($c.$level) { "[$($color[$level])]$($level.Substring(0, 1))$($c.$level)[/]" } }) & $addRow $table @( "[bold]$(& $escape $c.Name)[/]`n[grey50]$(& $escape $c.ResourceGroup) $($glyph.Dot) $(& $escape $c.Subscription)[/]" "$(& $escape $c.Version)`n[$(if ($c.Support -like 'Out*') { 'red1' } elseif ($c.Support -eq 'Preview') { 'orange1' } else { 'green3' })]$(& $escape $c.Support)[/]$(if ($c.Upgrade) { "`n[grey50]$($glyph.Arrow) $(& $escape $c.Upgrade)[/]" })" "$($c.Nodes)`n[grey50]max $($c.MaxNodes)[/]" "[grey85]$(& $escape $c.Network)[/]`n[grey50]$(& $escape $c.ApiServer)[/]" (& $score $c.Reliability); (& $score $c.Security); (& $score $c.CostOptimization); (& $score $c.OperationalExcellence); (& $score $c.PerformanceEfficiency) $(if ($counts) { $counts -join ' ' } else { "[green3]$($glyph.Tick)[/]" }) ) } [Spectre.Console.AnsiConsole]::Write($table) [Spectre.Console.AnsiConsole]::WriteLine() $bars = @($Assessment.Pillars | Where-Object { $null -ne $_.Score } | ForEach-Object { @{ Label = $_.Pillar; Value = $_.Score } }) if ($bars.Count) { Show-AACBarChart -Item $bars -Title 'Well-Architected checks passed per pillar (%)'; [Spectre.Console.AnsiConsole]::WriteLine() } $serious = @($Assessment.Findings | Where-Object { $_.Severity -in 'High', 'Medium' }) if ($serious.Count) { Write-AACMarkup "[bold]$($glyph.Bullet) Findings[/] [grey58]$($glyph.Dot) High and Medium, $($serious.Count) in all[/]" foreach ($f in $serious | Select-Object -First 40) { Write-AACMarkup " [$($color[$f.Severity])]$($f.Severity.ToUpperInvariant().PadRight(6))[/] [white]$(& $escape $f.Cluster)[/] [grey62]$(& $escape $f.Check)[/] [grey42]$(& $escape $f.Pillar) $($glyph.Dot) $(& $escape $f.Source)[/]" Write-AACMarkup " [grey85]$(& $escape $f.Detail)[/]" if ($f.Recommendation) { Write-AACMarkup " [grey50]$($glyph.Arrow) $(& $escape $f.Recommendation)[/]" } } if ($serious.Count -gt 40) { Write-AACMarkup " [grey50]... and $($serious.Count - 40) more: -HtmlPath has them all.[/]" } [Spectre.Console.AnsiConsole]::WriteLine() } if ($policy -and @($policy.ByNamespace).Count) { $table = & $newTable "[bold]$($glyph.Bullet) Azure Policy for Kubernetes by namespace[/] [grey58]$($glyph.Dot) $($policy.Stats.Violations) violation(s), $($policy.Stats.Workloads) workload(s)[/]" @('Namespace', 'Violations', 'Workloads', 'Under Deny', 'Policies violated') @('Violations', 'Workloads', 'Under Deny') foreach ($row in $policy.ByNamespace | Select-Object -First 15) { & $addRow $table @("[white]$(& $escape $row.Namespace)[/]", "$($row.Violations)", "$($row.Workloads)", $(if ($row.Deny) { "[red1]$($row.Deny)[/]" } else { '0' }), "[grey70]$(& $escape $row.PolicyNames)[/]") } [Spectre.Console.AnsiConsole]::Write($table) [Spectre.Console.AnsiConsole]::WriteLine() } if (@($Assessment.Clusters).Count -le $Detail) { foreach ($c in $Assessment.Clusters) { Write-AACRule -Title "[bold]$(& $escape $c.Name)[/] [grey58]Kubernetes $(& $escape $c.Version)[/]" -Color 'grey50' $table = & $newTable '[bold]Node pools[/]' @('Pool', 'Size', 'Nodes', 'Autoscale', 'Zones', 'Version / image', 'OS disk') @('Nodes') foreach ($pool in @($Assessment.NodePools | Where-Object ClusterId -EQ $c.ResourceId)) { & $addRow $table @("[white]$(& $escape $pool.Pool)[/] [grey50]$(& $escape $pool.Mode)[/]", "$(& $escape $pool.VmSize)`n[grey50]$(& $escape $pool.OsSku)[/]", "$($pool.Nodes)", "$(& $escape $pool.Autoscale)$(if ($pool.Autoscale -eq 'Yes') { " [grey50]$($pool.Min)-$($pool.Max)[/]" })", $(if ($pool.Zones) { & $escape $pool.Zones } else { '[orange1]none[/]' }), "$(& $escape $pool.Version)`n[grey50]image $(if ($null -ne $pool.NodeImageAge) { "$($pool.NodeImageAge) days old" } else { '?' })[/]", (& $escape $pool.OsDiskType)) } [Spectre.Console.AnsiConsole]::Write($table) $failed = @($Assessment.Checks | Where-Object { $_.ClusterId -eq $c.ResourceId -and $_.Status -eq 'Fail' }) Write-AACMarkup "[grey58]Well-Architected:[/] $(@($Assessment.Checks | Where-Object { $_.ClusterId -eq $c.ResourceId -and $_.Status -eq 'Pass' }).Count) passed, [orange1]$($failed.Count) failed[/]$(if ($failed.Count) { ": $(& $escape ((@($failed | ForEach-Object Check)) -join '; '))" })" [Spectre.Console.AnsiConsole]::WriteLine() } } else { Write-AACMarkup '[grey42]-Name shows a cluster in detail; -HtmlPath or -PdfPath has every cluster, setting and check.[/]' } Write-AACMarkup '[grey42]Add -PassThru (or pipe the command) for the objects; -CsvPath writes a CSV per table.[/]' } |