Private/radar-artifact-inventory.ps1
|
function Get-KubeBuddyImageParts { param( [string]$Image ) $raw = [string]$Image if ([string]::IsNullOrWhiteSpace($raw)) { return @{ fullRef = '' name = '' tag = '' digest = '' currentVersion = '' } } $fullRef = $raw.Trim() $nameTag = $fullRef $digest = '' if ($nameTag.Contains('@')) { $split = $nameTag.Split('@', 2) $nameTag = $split[0] $digest = if ($split.Count -gt 1) { [string]$split[1] } else { '' } } $lastSlash = $nameTag.LastIndexOf('/') $lastColon = $nameTag.LastIndexOf(':') $name = $nameTag $tag = '' if ($lastColon -gt $lastSlash) { $name = $nameTag.Substring(0, $lastColon) $tag = $nameTag.Substring($lastColon + 1) } $currentVersion = if ($tag) { $tag } elseif ($digest) { $digest } else { '' } return @{ fullRef = $fullRef name = $name tag = $tag digest = $digest currentVersion = $currentVersion } } function Get-KubeBuddyArtifactLabelValue { param( [hashtable]$Labels, [string[]]$Keys ) if (-not $Labels -or -not $Keys) { return '' } foreach ($k in $Keys) { if ($Labels.ContainsKey($k) -and -not [string]::IsNullOrWhiteSpace([string]$Labels[$k])) { return [string]$Labels[$k] } } return '' } function Get-KubeBuddyMetadataMap { param( [object]$Metadata ) $labels = @{} $annotations = @{} if ($Metadata -and $Metadata.labels) { foreach ($p in $Metadata.labels.PSObject.Properties) { $labels[[string]$p.Name] = [string]$p.Value } } if ($Metadata -and $Metadata.annotations) { foreach ($p in $Metadata.annotations.PSObject.Properties) { $annotations[[string]$p.Name] = [string]$p.Value } } return @{ labels = $labels annotations = $annotations } } function Get-KubeBuddyRadarArtifactInventory { param( [object]$KubeData, [switch]$ExcludeNamespaces ) $imagesByKey = @{} $helmByKey = @{} $appsByKey = @{} $inventorySource = if ($KubeData -and $KubeData.RawArtifactInventory) { $KubeData.RawArtifactInventory } else { $KubeData } $excludedNamespaces = @() $excludedSet = @{} $omittedWorkloads = 0 $omittedNamespaceMap = @{} if ($ExcludeNamespaces) { $excludedNamespaces = @(Get-ExcludedNamespaces) foreach ($ns in $excludedNamespaces) { if (-not [string]::IsNullOrWhiteSpace([string]$ns)) { $excludedSet[[string]$ns.ToLowerInvariant()] = $true } } } $workloadSets = @( @{ Kind = 'Deployment'; Items = @($inventorySource.Deployments) }, @{ Kind = 'StatefulSet'; Items = @($inventorySource.StatefulSets) }, @{ Kind = 'DaemonSet'; Items = @($inventorySource.DaemonSets) }, @{ Kind = 'Job'; Items = @($inventorySource.Jobs) }, @{ Kind = 'CronJob'; Items = @($inventorySource.CronJobs) }, @{ Kind = 'Pod'; Items = @($inventorySource.Pods) } ) foreach ($set in $workloadSets) { foreach ($item in @($set.Items)) { if (-not $item) { continue } $kind = [string]$set.Kind $metadata = $item.metadata $namespace = if ($metadata.namespace) { [string]$metadata.namespace } else { 'cluster-wide' } $workloadName = if ($metadata.name) { [string]$metadata.name } else { 'unknown' } if ($ExcludeNamespaces -and $excludedSet.ContainsKey($namespace.ToLowerInvariant())) { $omittedWorkloads++ $omittedNamespaceMap[$namespace] = $true continue } $metaMaps = Get-KubeBuddyMetadataMap -Metadata $metadata $labels = @{} $annotations = @{} foreach ($k in $metaMaps.labels.Keys) { $labels[$k] = $metaMaps.labels[$k] } foreach ($k in $metaMaps.annotations.Keys) { $annotations[$k] = $metaMaps.annotations[$k] } $spec = $item.spec $templateMetadata = $null $podSpec = $null if ($kind -eq 'Pod') { $podSpec = $spec } elseif ($kind -eq 'CronJob') { $templateMetadata = $spec.jobTemplate.spec.template.metadata $podSpec = $spec.jobTemplate.spec.template.spec } else { $templateMetadata = $spec.template.metadata $podSpec = $spec.template.spec } if ($templateMetadata) { $templateMaps = Get-KubeBuddyMetadataMap -Metadata $templateMetadata foreach ($k in $templateMaps.labels.Keys) { if (-not $labels.ContainsKey($k)) { $labels[$k] = $templateMaps.labels[$k] } } foreach ($k in $templateMaps.annotations.Keys) { if (-not $annotations.ContainsKey($k)) { $annotations[$k] = $templateMaps.annotations[$k] } } } $helmChartLabel = Get-KubeBuddyArtifactLabelValue -Labels $labels -Keys @('helm.sh/chart') $helmManagedBy = Get-KubeBuddyArtifactLabelValue -Labels $labels -Keys @('app.kubernetes.io/managed-by') $helmReleaseName = if ($annotations.ContainsKey('meta.helm.sh/release-name')) { [string]$annotations['meta.helm.sh/release-name'] } else { '' } $helmReleaseNs = if ($annotations.ContainsKey('meta.helm.sh/release-namespace')) { [string]$annotations['meta.helm.sh/release-namespace'] } else { '' } $isHelmManaged = [bool]($helmChartLabel -or ($helmManagedBy -and $helmManagedBy.ToLowerInvariant() -eq 'helm') -or $helmReleaseName) $helmChartName = '' $helmChartVersion = '' if ($helmChartLabel -match '^(?<name>.+)-(?<version>v?\d[\w\.\-\+]*)$') { $helmChartName = [string]$matches.name $helmChartVersion = [string]$matches.version } elseif ($helmChartLabel) { $helmChartName = [string]$helmChartLabel } $managedByValue = Get-KubeBuddyArtifactLabelValue -Labels $labels -Keys @('app.kubernetes.io/managed-by') $partOfValue = Get-KubeBuddyArtifactLabelValue -Labels $labels -Keys @('app.kubernetes.io/part-of') $controllerOwnerName = Get-KubeBuddyArtifactLabelValue -Labels $labels -Keys @( 'gateway.envoyproxy.io/owning-gateway-name', 'argocd.argoproj.io/instance', 'kustomize.toolkit.fluxcd.io/name' ) $controllerOwnerNamespace = Get-KubeBuddyArtifactLabelValue -Labels $labels -Keys @( 'gateway.envoyproxy.io/owning-gateway-namespace', 'kustomize.toolkit.fluxcd.io/namespace' ) $isControllerManaged = [bool]( $managedByValue -and $managedByValue.ToLowerInvariant() -ne 'helm' -and $managedByValue.ToLowerInvariant() -ne 'kubernetes' ) $appName = Get-KubeBuddyArtifactLabelValue -Labels $labels -Keys @('app.kubernetes.io/name', 'app') $appVersion = Get-KubeBuddyArtifactLabelValue -Labels $labels -Keys @('app.kubernetes.io/version', 'app.kubernetes.io/app-version', 'appVersion', 'version') if ($appName -and $appVersion) { $appKey = ("{0}|{1}|{2}" -f $appName.ToLowerInvariant(), $appVersion.ToLowerInvariant(), $namespace.ToLowerInvariant()) if (-not $appsByKey.ContainsKey($appKey)) { $appsByKey[$appKey] = [PSCustomObject]@{ name = $appName version = $appVersion namespace = $namespace workloadKind = $kind workloadName = $workloadName source = 'k8s_labels' managedByHelm = $isHelmManaged helmChartName = $helmChartName helmReleaseName = $helmReleaseName managedBy = $managedByValue managedByController = $isControllerManaged controllerOwnerName = $controllerOwnerName controllerOwnerNamespace = if ($controllerOwnerNamespace) { $controllerOwnerNamespace } else { $namespace } partOf = $partOfValue } } } if ($helmChartLabel -or ($helmManagedBy -and $helmManagedBy.ToLowerInvariant() -eq 'helm')) { $chartName = $helmChartName $chartVersion = $helmChartVersion if (-not $chartName) { $chartName = if ($appName) { $appName } else { $workloadName } } $helmKey = ("{0}|{1}|{2}|{3}" -f $chartName.ToLowerInvariant(), $chartVersion.ToLowerInvariant(), $helmReleaseName.ToLowerInvariant(), $namespace.ToLowerInvariant()) if (-not $helmByKey.ContainsKey($helmKey)) { $helmByKey[$helmKey] = [PSCustomObject]@{ name = $chartName version = $chartVersion releaseName = $helmReleaseName releaseNamespace = if ($helmReleaseNs) { $helmReleaseNs } else { $namespace } namespace = $namespace workloadKind = $kind workloadName = $workloadName source = if ($helmChartLabel) { 'helm.sh/chart' } else { 'app.kubernetes.io/managed-by=Helm' } } } } if (-not $podSpec) { continue } $containers = @($podSpec.containers) + @($podSpec.initContainers) foreach ($container in @($containers)) { if (-not $container) { continue } $imageRef = [string]$container.image if ([string]::IsNullOrWhiteSpace($imageRef)) { continue } $parts = Get-KubeBuddyImageParts -Image $imageRef if (-not $parts.fullRef) { continue } $imageKey = $parts.fullRef.ToLowerInvariant() if (-not $imagesByKey.ContainsKey($imageKey)) { $imagesByKey[$imageKey] = [PSCustomObject]@{ fullRef = $parts.fullRef name = $parts.name tag = $parts.tag digest = $parts.digest currentVersion = $parts.currentVersion namespace = $namespace workloadKind = $kind workloadName = $workloadName containerName = [string]$container.name source = 'workload_spec' managedByHelm = $isHelmManaged helmChartName = $helmChartName helmReleaseName = $helmReleaseName managedBy = $managedByValue managedByController = $isControllerManaged controllerOwnerName = $controllerOwnerName controllerOwnerNamespace = if ($controllerOwnerNamespace) { $controllerOwnerNamespace } else { $namespace } partOf = $partOfValue } } } } } $images = @($imagesByKey.Values | Sort-Object fullRef) $helmCharts = @($helmByKey.Values | Sort-Object name, version, namespace) $apps = @($appsByKey.Values | Sort-Object name, version, namespace) return @{ images = $images helmCharts = $helmCharts apps = $apps meta = @{ excludedNamespacesApplied = [bool]$ExcludeNamespaces excludedNamespaces = @($excludedNamespaces) omittedWorkloads = [int]$omittedWorkloads omittedNamespaces = @($omittedNamespaceMap.Keys | Sort-Object) } summary = @{ images = $images.Count helmCharts = $helmCharts.Count apps = $apps.Count total = $images.Count + $helmCharts.Count + $apps.Count } } } function Get-KubeBuddyRadarFreshnessLookup { param( [object]$Freshness ) $lookup = @{} if (-not $Freshness -or -not $Freshness.items) { return $lookup } foreach ($item in @($Freshness.items)) { if (-not $item) { continue } $artifactType = [string]($item.artifact_type ?? '') $artifactKey = [string]($item.artifact_key ?? '') $currentVersion = Normalize-KubeBuddyRadarVersion -Version ([string]($item.current_version ?? '')) if ([string]::IsNullOrWhiteSpace($artifactType) -or [string]::IsNullOrWhiteSpace($artifactKey)) { continue } $fullKey = ("{0}|{1}|{2}" -f $artifactType.ToLowerInvariant(), $artifactKey.ToLowerInvariant(), $currentVersion.ToLowerInvariant()) $baseKey = ("{0}|{1}" -f $artifactType.ToLowerInvariant(), $artifactKey.ToLowerInvariant()) $lookup[$fullKey] = $item $lookup[$baseKey] = $item } return $lookup } function Get-KubeBuddyRadarArtifactFreshnessItem { param( [hashtable]$FreshnessLookup, [string]$ArtifactType, [string]$ArtifactKey, [string]$CurrentVersion ) if (-not $FreshnessLookup) { return $null } $normalizedVersion = Normalize-KubeBuddyRadarVersion -Version ([string]$CurrentVersion) $fullKey = ("{0}|{1}|{2}" -f $ArtifactType.ToLowerInvariant(), $ArtifactKey.ToLowerInvariant(), $normalizedVersion) if ($FreshnessLookup.ContainsKey($fullKey)) { return $FreshnessLookup[$fullKey] } $baseKey = ("{0}|{1}" -f $ArtifactType.ToLowerInvariant(), $ArtifactKey.ToLowerInvariant()) if ($FreshnessLookup.ContainsKey($baseKey)) { return $FreshnessLookup[$baseKey] } return $null } function Normalize-KubeBuddyRadarVersion { param( [string]$Version ) $v = [string]$Version if ([string]::IsNullOrWhiteSpace($v)) { return '' } $trimmed = $v.Trim().ToLowerInvariant() if ($trimmed.StartsWith('v') -and $trimmed.Length -gt 1 -and [char]::IsDigit($trimmed[1])) { return $trimmed.Substring(1) } return $trimmed } function Merge-KubeBuddyHelmChartRows { param( [object[]]$HelmCharts ) $rows = @($HelmCharts) if ($rows.Count -eq 0) { return @() } $statusRank = @{ 'major_behind' = 4 'minor_behind' = 3 'unknown' = 2 'up_to_date' = 1 } $map = @{} foreach ($chart in $rows) { if (-not $chart) { continue } $name = [string]($chart.name ?? '') $ns = [string]($chart.namespace ?? $chart.releaseNamespace ?? '') $key = ("{0}|{1}" -f $name.ToLowerInvariant(), $ns.ToLowerInvariant()) if ([string]::IsNullOrWhiteSpace($name)) { continue } if (-not $map.ContainsKey($key)) { $map[$key] = $chart continue } $cur = $map[$key] $curStatus = [string]($cur.freshnessStatus ?? 'unknown') $nextStatus = [string]($chart.freshnessStatus ?? 'unknown') $curRank = if ($statusRank.ContainsKey($curStatus)) { [int]$statusRank[$curStatus] } else { 0 } $nextRank = if ($statusRank.ContainsKey($nextStatus)) { [int]$statusRank[$nextStatus] } else { 0 } if ($nextRank -gt $curRank) { $map[$key] = $chart $cur = $chart } if ([string]::IsNullOrWhiteSpace([string]($cur.releaseName ?? '')) -and -not [string]::IsNullOrWhiteSpace([string]($chart.releaseName ?? ''))) { $cur.releaseName = [string]$chart.releaseName } if ([string]::IsNullOrWhiteSpace([string]($cur.latestVersion ?? '')) -and -not [string]::IsNullOrWhiteSpace([string]($chart.latestVersion ?? ''))) { $cur.latestVersion = [string]$chart.latestVersion } } return @($map.Values | Sort-Object name, namespace) } function Get-KubeBuddyRadarArtifactRecommendation { param( [string]$Status, [string]$Latest, [bool]$IsMonitored = $false ) $statusNorm = ([string]$Status).Trim().ToLowerInvariant() $latestNorm = ([string]$Latest).Trim() if ($statusNorm -eq 'major_behind' -or $statusNorm -eq 'minor_behind') { if (-not [string]::IsNullOrWhiteSpace($latestNorm) -and $latestNorm -ne 'not monitored') { return "Update recommended. Target $latestNorm and review breaking changes." } return "Update recommended. Review latest release notes and breaking changes." } if ($statusNorm -eq 'up_to_date') { return "Up to date. Keep monitoring for new releases." } if ($IsMonitored -or (-not [string]::IsNullOrWhiteSpace($latestNorm) -and $latestNorm -ne 'not monitored')) { return "Best-effort version compare. Review release notes before updating." } return "Not monitored in catalog yet. Request this artifact to be added." } function Convert-KubeBuddyRadarArtifactInventoryToText { param( [hashtable]$Inventory, [object]$Freshness ) return @() $lines = @() $freshnessLookup = Get-KubeBuddyRadarFreshnessLookup -Freshness $Freshness $helmChartsMerged = Merge-KubeBuddyHelmChartRows -HelmCharts @($Inventory.helmCharts) $lines += "" $lines += "[📦 Outdated Artifacts]" $lines += "Best-effort version matching from chart/image names and tags. Results are indicative, not guaranteed 100% accurate." $lines += "Helm Charts: $($helmChartsMerged.Count) | Images: $($Inventory.summary.images)" if ($Inventory.meta -and [int]($Inventory.meta.omittedWorkloads ?? 0) -gt 0) { $lines += "Excluded namespaces omitted $([int]$Inventory.meta.omittedWorkloads) workload(s) from inventory: $((@($Inventory.meta.omittedNamespaces) -join ', '))" } if ($Freshness -and $Freshness.summary) { $lines += "Freshness: Up-to-date $($Freshness.summary.up_to_date) | Minor behind $($Freshness.summary.minor_behind) | Major behind $($Freshness.summary.major_behind) | Unknown $($Freshness.summary.unknown)" } $lines += "" $lines += "[Helm Charts]" if ($helmChartsMerged.Count -eq 0) { $lines += "- None found." } else { foreach ($chart in $helmChartsMerged) { $version = if ($chart.version) { $chart.version } else { 'unknown' } $release = if ($chart.releaseName) { $chart.releaseName } else { 'unknown' } $freshnessItem = Get-KubeBuddyRadarArtifactFreshnessItem -FreshnessLookup $freshnessLookup -ArtifactType 'helm_chart' -ArtifactKey ([string]$chart.name) -CurrentVersion ([string]$version) $latest = if ($freshnessItem -and $freshnessItem.latest_version) { [string]$freshnessItem.latest_version } elseif ($chart.latestVersion) { [string]$chart.latestVersion } else { 'not monitored' } $status = if ($freshnessItem -and $freshnessItem.status) { [string]$freshnessItem.status } elseif ($chart.freshnessStatus) { [string]$chart.freshnessStatus } else { 'unknown' } $isMonitored = ($freshnessItem -and ( -not [string]::IsNullOrWhiteSpace([string]($freshnessItem.latest_version ?? '')) -or -not [string]::IsNullOrWhiteSpace([string]($freshnessItem.source ?? '')) )) -or ($latest -ne 'not monitored') $recommendation = Get-KubeBuddyRadarArtifactRecommendation -Status $status -Latest $latest -IsMonitored:$isMonitored $lines += "- Chart: $($chart.name) | Version: $version | Latest: $latest | Status: $status | Release: $release | Namespace: $($chart.namespace) | Workload: $($chart.workloadKind)/$($chart.workloadName) | Recommendation: $recommendation" } } $lines += "" $lines += "[Container Images]" if ($Inventory.images.Count -eq 0) { $lines += "- None found." } else { foreach ($img in $Inventory.images) { $freshnessItem = Get-KubeBuddyRadarArtifactFreshnessItem -FreshnessLookup $freshnessLookup -ArtifactType 'image' -ArtifactKey ([string]$img.fullRef) -CurrentVersion ([string]$img.currentVersion) if ($freshnessItem -and [bool]($freshnessItem.inherited_from_helm ?? $false)) { continue } if ($freshnessItem -and [string]($freshnessItem.status ?? '') -eq 'covered_by_controller') { continue } $latest = if ($freshnessItem -and $freshnessItem.latest_version) { [string]$freshnessItem.latest_version } elseif ($img.latestVersion) { [string]$img.latestVersion } else { 'not monitored' } $status = if ($freshnessItem -and $freshnessItem.status) { [string]$freshnessItem.status } elseif ($img.freshnessStatus) { [string]$img.freshnessStatus } else { 'unknown' } $isMonitored = ($freshnessItem -and ( -not [string]::IsNullOrWhiteSpace([string]($freshnessItem.latest_version ?? '')) -or -not [string]::IsNullOrWhiteSpace([string]($freshnessItem.source ?? '')) )) -or ($latest -ne 'not monitored') $recommendation = Get-KubeBuddyRadarArtifactRecommendation -Status $status -Latest $latest -IsMonitored:$isMonitored $lines += "- Image: $($img.fullRef) | Version: $($img.currentVersion) | Latest: $latest | Status: $status | Namespace: $($img.namespace) | Workload: $($img.workloadKind)/$($img.workloadName) | Container: $($img.containerName) | Recommendation: $recommendation" } } return $lines } function Convert-KubeBuddyRadarArtifactInventoryToHtml { param( [hashtable]$Inventory, [object]$Freshness ) return "" $freshnessLookup = Get-KubeBuddyRadarFreshnessLookup -Freshness $Freshness $helmChartsMerged = Merge-KubeBuddyHelmChartRows -HelmCharts @($Inventory.helmCharts) $freshnessSummaryHtml = "" if ($Freshness -and $Freshness.summary) { $freshnessSummaryHtml = @" <div class="hero-metrics"> <div class="metric-card normal"><div class="card-content"><p>✅ Up to date: <strong>$($Freshness.summary.up_to_date)</strong></p></div></div> <div class="metric-card warning"><div class="card-content"><p>🟨 Minor behind: <strong>$($Freshness.summary.minor_behind)</strong></p></div></div> <div class="metric-card critical"><div class="card-content"><p>🟥 Major behind: <strong>$($Freshness.summary.major_behind)</strong></p></div></div> <div class="metric-card default"><div class="card-content"><p>❔ Unknown: <strong>$($Freshness.summary.unknown)</strong></p></div></div> </div> "@ } $imageRowsCollection = @() $omittedImagesCoveredByHelm = 0 if ($Inventory.images.Count -gt 0) { foreach ($img in $Inventory.images) { $freshnessItem = Get-KubeBuddyRadarArtifactFreshnessItem -FreshnessLookup $freshnessLookup -ArtifactType 'image' -ArtifactKey ([string]$img.fullRef) -CurrentVersion ([string]$img.currentVersion) if ($freshnessItem -and [bool]($freshnessItem.inherited_from_helm ?? $false)) { $omittedImagesCoveredByHelm++ continue } if ($freshnessItem -and [string]($freshnessItem.status ?? '') -eq 'covered_by_controller') { $omittedImagesCoveredByHelm++ continue } $latest = if ($freshnessItem -and $freshnessItem.latest_version) { [string]$freshnessItem.latest_version } elseif ($img.latestVersion) { [string]$img.latestVersion } else { 'not monitored' } $status = if ($freshnessItem -and $freshnessItem.status) { [string]$freshnessItem.status } elseif ($img.freshnessStatus) { [string]$img.freshnessStatus } else { 'unknown' } $isMonitored = ($freshnessItem -and ( -not [string]::IsNullOrWhiteSpace([string]($freshnessItem.latest_version ?? '')) -or -not [string]::IsNullOrWhiteSpace([string]($freshnessItem.source ?? '')) )) -or ($latest -ne 'not monitored') $recommendation = Get-KubeBuddyRadarArtifactRecommendation -Status $status -Latest $latest -IsMonitored:$isMonitored $imageRowsCollection += "<tr><td>$($img.fullRef)</td><td>$($img.currentVersion)</td><td>$latest</td><td>$status</td><td>$($img.namespace)</td><td>$($img.workloadKind)/$($img.workloadName)</td><td>$($img.containerName)</td><td>$recommendation</td></tr>" } } $imageRows = if ($imageRowsCollection.Count -gt 0) { $imageRowsCollection -join "`n" } else { "<tr><td colspan='8'>No standalone container images detected (all covered by Helm chart checks or none detected).</td></tr>" } $chartRows = if ($helmChartsMerged.Count -gt 0) { ($helmChartsMerged | ForEach-Object { $version = if ($_.version) { $_.version } else { 'unknown' } $release = if ($_.releaseName) { $_.releaseName } else { 'unknown' } $freshnessItem = Get-KubeBuddyRadarArtifactFreshnessItem -FreshnessLookup $freshnessLookup -ArtifactType 'helm_chart' -ArtifactKey ([string]$_.name) -CurrentVersion ([string]$version) $latest = if ($freshnessItem -and $freshnessItem.latest_version) { [string]$freshnessItem.latest_version } elseif ($_.latestVersion) { [string]$_.latestVersion } else { 'not monitored' } $status = if ($freshnessItem -and $freshnessItem.status) { [string]$freshnessItem.status } elseif ($_.freshnessStatus) { [string]$_.freshnessStatus } else { 'unknown' } $isMonitored = ($freshnessItem -and ( -not [string]::IsNullOrWhiteSpace([string]($freshnessItem.latest_version ?? '')) -or -not [string]::IsNullOrWhiteSpace([string]($freshnessItem.source ?? '')) )) -or ($latest -ne 'not monitored') $recommendation = Get-KubeBuddyRadarArtifactRecommendation -Status $status -Latest $latest -IsMonitored:$isMonitored "<tr><td>$($_.name)</td><td>$version</td><td>$latest</td><td>$status</td><td>$release</td><td>$($_.namespace)</td><td>$($_.workloadKind)/$($_.workloadName)</td><td>$recommendation</td></tr>" }) -join "`n" } else { "<tr><td colspan='8'>No Helm charts detected.</td></tr>" } return @" <h2>Outdated Artifacts</h2> <p>This section is included because Radar mode is enabled for this run. It captures deterministic image, Helm chart, and app versions from Kubernetes workload specs and labels.</p> <p><strong>Best effort:</strong> version matching is based on artifact names and tags. Results are indicative and may not be 100% accurate.</p> $(if ($Inventory.meta -and [int]($Inventory.meta.omittedWorkloads ?? 0) -gt 0) { "<p><strong>Excluded namespaces:</strong> omitted $([int]$Inventory.meta.omittedWorkloads) workload(s) from inventory: $((@($Inventory.meta.omittedNamespaces) -join ', ')).</p>" } else { "" }) $freshnessSummaryHtml <div class="hero-metrics"> <div class="metric-card default"><div class="card-content"><p>⎈ Helm Charts: <strong>$($helmChartsMerged.Count)</strong></p></div></div> <div class="metric-card default"><div class="card-content"><p>🧱 Images: <strong>$($Inventory.summary.images)</strong></p></div></div> <div class="metric-card default"><div class="card-content"><p>📊 Tracked: <strong>$([int]$Inventory.summary.images + [int]$helmChartsMerged.Count)</strong></p></div></div> </div> <h3>Helm Charts</h3> <div class="table-container"> <table> <thead><tr><th>Chart</th><th>Version</th><th>Latest</th><th>Status</th><th>Release</th><th>Namespace</th><th>Workload</th><th>Recommendation</th></tr></thead> <tbody> $chartRows </tbody> </table> </div> <h3>Container Images</h3> <div class="table-container"> <table> <thead><tr><th>Image</th><th>Version</th><th>Latest</th><th>Status</th><th>Namespace</th><th>Workload</th><th>Container</th><th>Recommendation</th></tr></thead> <tbody> $imageRows </tbody> </table> </div> "@ } function Update-KubeBuddyJsonReportWithRadarFreshness { param( [string]$ReportPath, [object]$Freshness ) if ([string]::IsNullOrWhiteSpace($ReportPath) -or -not (Test-Path $ReportPath) -or -not $Freshness) { return } try { $json = Get-Content -Raw -Path $ReportPath | ConvertFrom-Json -Depth 60 if (-not $json.radar) { $json | Add-Member -NotePropertyName radar -NotePropertyValue ([PSCustomObject]@{}) -Force } $json.radar | Add-Member -NotePropertyName freshness -NotePropertyValue $Freshness -Force $json.radar | Add-Member -NotePropertyName freshnessFetchedAt -NotePropertyValue ((Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")) -Force $lookup = Get-KubeBuddyRadarFreshnessLookup -Freshness $Freshness if ($json.artifacts) { foreach ($img in @($json.artifacts.images)) { if (-not $img) { continue } $current = [string]($img.currentVersion ?? '') $match = Get-KubeBuddyRadarArtifactFreshnessItem -FreshnessLookup $lookup -ArtifactType 'image' -ArtifactKey ([string]$img.fullRef) -CurrentVersion $current $img | Add-Member -NotePropertyName latestVersion -NotePropertyValue ([string]($match.latest_version ?? '')) -Force $img | Add-Member -NotePropertyName freshnessStatus -NotePropertyValue ([string]($match.status ?? 'unknown')) -Force $img | Add-Member -NotePropertyName freshnessConfidence -NotePropertyValue ([double]($match.confidence ?? 0)) -Force $img | Add-Member -NotePropertyName freshnessReason -NotePropertyValue ([string]($match.reason ?? '')) -Force $img | Add-Member -NotePropertyName freshnessSource -NotePropertyValue ([string]($match.source ?? '')) -Force $img | Add-Member -NotePropertyName globalLatestVersion -NotePropertyValue ([string]($match.global_latest_version ?? '')) -Force $img | Add-Member -NotePropertyName compareMode -NotePropertyValue ([string]($match.compare_mode ?? '')) -Force $img | Add-Member -NotePropertyName inheritedFromHelm -NotePropertyValue ([bool]($match.inherited_from_helm ?? $false)) -Force } foreach ($chart in @($json.artifacts.helmCharts)) { if (-not $chart) { continue } $current = [string]($chart.version ?? '') $match = Get-KubeBuddyRadarArtifactFreshnessItem -FreshnessLookup $lookup -ArtifactType 'helm_chart' -ArtifactKey ([string]$chart.name) -CurrentVersion $current $chart | Add-Member -NotePropertyName latestVersion -NotePropertyValue ([string]($match.latest_version ?? '')) -Force $chart | Add-Member -NotePropertyName freshnessStatus -NotePropertyValue ([string]($match.status ?? 'unknown')) -Force $chart | Add-Member -NotePropertyName freshnessConfidence -NotePropertyValue ([double]($match.confidence ?? 0)) -Force $chart | Add-Member -NotePropertyName freshnessReason -NotePropertyValue ([string]($match.reason ?? '')) -Force $chart | Add-Member -NotePropertyName freshnessSource -NotePropertyValue ([string]($match.source ?? '')) -Force $chart | Add-Member -NotePropertyName globalLatestVersion -NotePropertyValue ([string]($match.global_latest_version ?? '')) -Force $chart | Add-Member -NotePropertyName compareMode -NotePropertyValue ([string]($match.compare_mode ?? '')) -Force $chart | Add-Member -NotePropertyName inheritedFromHelm -NotePropertyValue ([bool]($match.inherited_from_helm ?? $false)) -Force } # Apps are intentionally not enriched in direct lookup mode to reduce noise. } $json | ConvertTo-Json -Depth 60 | Set-Content -Encoding UTF8 -Path $ReportPath } catch { Write-Host "⚠️ Could not update JSON report with Radar freshness data: $($_.Exception.Message)" -ForegroundColor Yellow } } |