Public/Get-PSUk8sPodLabel.ps1

<#
.SYNOPSIS
    Gets pod labels from AKS clusters in parallel with minimal kubectl overhead.
 
.DESCRIPTION
    Retrieves pod labels from all namespaces in AKS clusters using a single optimized
    kubectl call per cluster. Shows live job progress and lists failed clusters for
    easy retry. Clusters are retrieved from the local kubeconfig file (~/.kube/config).
    Ensure all required cluster credentials are updated in the kubeconfig file before running.
 
.PARAMETER ClusterFilter
    (Optional) Filter clusters by name pattern using wildcards.
    Default: "*" (all clusters).
    Example: "*prod*" matches all production clusters.
 
.PARAMETER ThrottleLimit
    (Optional) Maximum number of parallel jobs to run simultaneously.
    Default: 8.
 
.EXAMPLE
    Get-PSUk8sPodLabel
 
    Retrieves pod labels from all clusters in the local kubeconfig.
 
.EXAMPLE
    Get-PSUk8sPodLabel -ClusterFilter "*prod*" -ThrottleLimit 15
 
    Retrieves pod labels from all production clusters with up to 15 parallel jobs.
 
.OUTPUTS
    [System.Object[]]
 
.NOTES
    Author: Lakshmanachari Panuganti
    Created: 19th August 2025
    Last Modified: 7th March 2026
    Version: 1.1
    Requires: kubectl, ThreadJob module
 
.LINK
    https://github.com/lakshmanachari-panuganti/OMG.PSUtilities/tree/main/OMG.PSUtilities.AzureCore
    https://www.linkedin.com/in/lakshmanachari-panuganti/
    https://www.powershellgallery.com/packages/OMG.PSUtilities.AzureCore
#>

function Get-PSUk8sPodLabel {
    [CmdletBinding()]
    param(
        [string]$ClusterFilter = "*",
        [int]$ThrottleLimit = 8
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] Parameters:"
        Write-Verbose " ClusterFilter = $ClusterFilter"
        Write-Verbose " ThrottleLimit = $ThrottleLimit"

        # kubectl and ThreadJob are optional dependencies of this module and are resolved
        # here rather than declared in the manifest, so the other exports stay usable
        # without them. Never install anything implicitly; tell the caller what to do.
        if (-not (Get-Command -Name 'kubectl' -ErrorAction SilentlyContinue)) {
            throw "kubectl was not found on PATH. Install the Kubernetes CLI, confirm 'kubectl version --client' runs, then try again."
        }

        if (-not (Get-Command -Name 'Start-ThreadJob' -ErrorAction SilentlyContinue)) {
            throw "Start-ThreadJob is not available. Install the ThreadJob module with: Install-Module ThreadJob -Scope CurrentUser"
        }

        $kubeConfigPath = Join-Path $env:USERPROFILE ".kube\config"

        if (-not (Test-Path $kubeConfigPath)) {
            throw "Kubeconfig file not found at $kubeConfigPath."
        }

        Write-Host "Note: Retrieving clusters from kubeconfig: $kubeConfigPath" -ForegroundColor Cyan
        Write-Host " Ensure all required cluster credentials are updated in the kubeconfig file before running." -ForegroundColor Yellow
    }

    process {
        try {
        Write-Verbose "Getting all available clusters..."
        $clusters = kubectl config get-contexts -o name | Where-Object { $_ -like '*-admin' }
        if ($ClusterFilter -and $ClusterFilter -ne "*") {
            $clusters = $clusters | Where-Object { $_ -like $ClusterFilter }
        }
        if (-not $clusters) {
            Write-Warning "No Kubernetes clusters found in kubeconfig."
            return
        }

        Write-Host "Processing $($clusters.Count) clusters in parallel (ThrottleLimit: $ThrottleLimit)..."

        $jobs = @()
        foreach ($cluster in $clusters) {
            while (($jobs | Where-Object { $_.State -eq 'Running' }).Count -ge $ThrottleLimit) {
                $completed = ($jobs | Where-Object { $_.State -eq 'Completed' }).Count
                $running   = ($jobs | Where-Object { $_.State -eq 'Running' }).Count
                $failed    = ($jobs | Where-Object { $_.State -eq 'Failed' }).Count
                $percent   = [math]::Round(($completed / $clusters.Count) * 100, 1)

                $color = if ($percent -lt 50) { "Yellow" } elseif ($percent -lt 91) { "Cyan" } else { "Green" }
                Write-Host ("`rJobs completed: {0} / {1} ({4,5}%) | Running: {2} | Failed: {3} " -f $completed, $clusters.Count, $running, $failed, $percent) -ForegroundColor $color -NoNewline

                Start-Sleep -Seconds 1
            }

            $jobs += Start-ThreadJob -Name $cluster -ArgumentList $cluster -ScriptBlock {
                param($cluster)
                try {
                    $output = kubectl --context $cluster get pods --all-namespaces `
                        -o jsonpath='{range .items[*]}{.metadata.namespace}{"|"}{.metadata.name}{"|"}{.metadata.labels}{"|"}{.status.phase}{"|"}{.metadata.creationTimestamp}{"\n"}{end}' 2>$null

                    $lines = $output -split "`n"
                    foreach ($line in $lines) {
                        if (-not $line) { continue }
                        $parts = $line -split '\|', 5
                        [PSCustomObject]@{
                            Cluster   = $cluster
                            Namespace = $parts[0]
                            PodName   = $parts[1]
                            Labels    = $parts[2] -replace '^map\[|\]$', '' -replace ' ', ','  | ConvertFrom-Json
                            Status    = $parts[3]
                            CreatedAt = [datetime]$parts[4]
                        }
                    }
                }
                catch {
                    Write-Error "Failed to process cluster '$cluster': $_"
                }
            }
        }

        # Wait for completion with progress
        $totalJobs = $jobs.Count
        while ($true) {
            $completed = ($jobs | Where-Object { $_.State -eq 'Completed' }).Count
            $running   = ($jobs | Where-Object { $_.State -eq 'Running' }).Count
            $failed    = ($jobs | Where-Object { $_.State -eq 'Failed' }).Count
            $percent   = [math]::Round(($completed / $totalJobs) * 100, 1)

            $color = if ($percent -lt 50) { "Yellow" } elseif ($percent -lt 91) { "Cyan" } else { "Green" }
            Write-Host ("`rJobs completed: {0} / {1} ({4,5}%) | Running: {2} | Failed: {3} " -f $completed, $totalJobs, $running, $failed, $percent) -ForegroundColor $color -NoNewline

            if ($completed + $failed -eq $totalJobs) { break }
            Start-Sleep -Seconds 1
        }
        Write-Host "" # Newline after loop

        # Gather results
        $allResults = @()
        $failedClusters = @()
        foreach ($job in $jobs) {
            if ($job.State -eq 'Completed') {
                $result = Receive-Job -Job $job
                if ($result) { $allResults += $result }
            } elseif ($job.State -eq 'Failed') {
                $failedClusters += $job.Name
            }
            Remove-Job -Job $job
        }

        # Summary
        if ($failedClusters.Count -gt 0) {
            Write-Host "Failed clusters: $($failedClusters -join ', ')" -ForegroundColor Red
        } else {
            Write-Host "All clusters processed successfully." -ForegroundColor Green
        }

        if (-not $allResults) {
            Write-Warning "No pods found in any cluster."
            return
        }

        Write-Host "Found $($allResults.Count) pods across $($clusters.Count) clusters" -ForegroundColor Green
        return $allResults

        } catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}