Public/Get-KubeNodeDescription.ps1

Function Get-KubeNodeDescription
    {
        <#
        .SYNOPSIS
            Returns a rich description of one or more Kubernetes nodes, including labels, taints, conditions, addresses, system info, allocated resources, and the non-terminated pods currently running on each node.
            This command is the detailed node inspection tool in KubePS and is used by other node-oriented commands to gather capacity and workload information before summarizing it for users.

        .EXAMPLE
            Get-KubeNodeDescription -Node worker-1
            # Returns the full metadata and scheduling information for worker-1.

        .EXAMPLE
            Get-KubeNodeDescription -Node worker-1 | Select-Object -ExpandProperty Conditions
            # Lists the current readiness, memory pressure, disk pressure, and PID pressure conditions for the node.

        .EXAMPLE
            Get-KubeNodeDescription -Node worker-1 | Select-Object -ExpandProperty AllocatedResources
            # Displays how much CPU and memory requests/limits are currently used by workloads on that node.
        #>

        [CmdletBinding()]
        param()

        DynamicParam
            {
                $DynamicParameterDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()

                $NewDynamicParams = @{
                    DynamicParameterDictionary = $DynamicParameterDictionary
                }

                $Nodes = Get-KubeNodes

                $ParamList = @(
                    @{
                        ParameterName                   = 'Node'
                        ParameterType                   = ([string[]])
                        Mandatory                       = $True
                        ValidateSet                     = [array]$Nodes.Name
                        Alias                           = 'Name'
                        ValueFromPipeline               = $True
                        ValueFromPipelineByPropertyName = $True
                        HelpMessage                     = 'The node to describe'
                        Position                        = 1
                        ParameterSetName                = 'Node'
                    }
                )

                ForEach($Param in $ParamList)
                    {
                        Add-DynamicParameter @NewDynamicParams @Param
                    }
                Return $DynamicParameterDictionary
            }

        Begin
            {
                [System.Collections.Generic.List[object]]$NodeNames = @()
            }
        Process
            {
                $NodeValues = $DynamicParameterDictionary['Node'].Value
                ForEach($NodeName in $NodeValues) { $NodeNames.Add($NodeName) }
            }
        End
            {
                ForEach($NodeName in $NodeNames)
                    {
                        $NodeItem = [k8s.CoreV1OperationsExtensions]::ReadNodeAsync(
                            $Script:KubePSDirectClient.CoreV1,
                            $NodeName
                        ).GetAwaiter().GetResult()
                        $Pods = [k8s.CoreV1OperationsExtensions]::ListPodForAllNamespacesAsync(
                            $Script:KubePSDirectClient.CoreV1,
                            $null,
                            $null,
                            "spec.nodeName=$NodeName"
                        ).GetAwaiter().GetResult().Items | Where-Object { $_.Status.Phase -notin @('Succeeded', 'Failed') }

                        $Capacity    = [ordered]@{}
                        $Allocatable = [ordered]@{}
                        ForEach($Resource in $NodeItem.Status.Capacity.Keys)
                            {
                                $Capacity[$Resource] = [string]$NodeItem.Status.Capacity[$Resource]
                            }
                        ForEach($Resource in $NodeItem.Status.Allocatable.Keys)
                            {
                                $Allocatable[$Resource] = [string]$NodeItem.Status.Allocatable[$Resource]
                            }

                        $ResourceTotals = @{ Requests = @{}; Limits = @{} }
                        $NonTerminatedPods = foreach ($PodItem in $Pods) {
                            $PodResources = @{ Requests = @{}; Limits = @{} }
                            ForEach($Container in $PodItem.Spec.Containers)
                                {
                                    ForEach($Metric in 'Requests', 'Limits')
                                        {
                                            $Quantities = $Container.Resources.$Metric
                                            If($null -eq $Quantities)
                                                {
                                                    Continue
                                                }
                                            ForEach($Resource in $Quantities.Keys)
                                                {
                                                    $Value                              = [string]$Quantities[$Resource]
                                                    $PodResources[$Metric][$Resource]   = [decimal]$PodResources[$Metric][$Resource] + (ConvertTo-KubePSResourceNumber $Value)
                                                    $ResourceTotals[$Metric][$Resource] = [decimal]$ResourceTotals[$Metric][$Resource] + (ConvertTo-KubePSResourceNumber $Value)
                                                }
                                        }
                                }

                            $Row = [ordered]@{
                                Namespace = $PodItem.Metadata.NamespaceProperty
                                Name = $PodItem.Metadata.Name 
                            }

                            ForEach($Resource in @('cpu', 'memory'))
                                {
                                    $PropertyName = If($Resource -eq 'cpu') { 'Cpu' } Else { 'Memory' }
                                    ForEach($Metric in 'Requests', 'Limits')
                                        {
                                            $Value                                  = [decimal]$PodResources[$Metric][$Resource]
                                            $CapacityValue                          = ConvertTo-KubePSResourceNumber ([string]$NodeItem.Status.Allocatable[$Resource])
                                            $Percent                                = If($CapacityValue -gt 0) { [math]::Round(($Value / $CapacityValue) * 100) } Else { $null }
                                            $DisplayValue                           = If($Value -eq 0) { $null } ElseIf($Resource -eq 'cpu') { "$([math]::Round($Value * 1000))m" } Else { "$([math]::Round($Value / 1MB, 2))Mi" }
                                            $Row["${PropertyName}${Metric}"]        = $DisplayValue
                                            $Row["${PropertyName}${Metric}Percent"] = $Percent
                                        }
                                }
                            $Row['Age'] = Resolve-KubeResourceAge -Timestamp ([string]$PodItem.Metadata.CreationTimestamp)
                            [pscustomobject]$Row
                        }

                        $AllocatedResources = foreach ($Resource in ($ResourceTotals.Requests.Keys + $ResourceTotals.Limits.Keys | Sort-Object -Unique)) {
                            $Row = [ordered]@{ Resource = $Resource }
                            ForEach($Metric in 'Requests', 'Limits')
                                {
                                    $Value                   = [decimal]$ResourceTotals[$Metric][$Resource]
                                    $NodeCapacity            = ConvertTo-KubePSResourceNumber ([string]$NodeItem.Status.Allocatable[$Resource])
                                    $Percent                 = If($NodeCapacity -gt 0) { [math]::Round(($Value / $NodeCapacity) * 100) } Else { $null }
                                    $Suffix                  = If($Resource -eq 'cpu') { 'm' } ElseIf($Resource -eq 'memory' -or $Resource -eq 'ephemeral-storage' -or $Resource -like 'hugepages-*') { 'B' } Else { '' }
                                    $DisplayValue            = If($Suffix -eq 'm') { "$([math]::Round($Value * 1000))m" } ElseIf($Suffix -eq 'B') { "$([math]::Round($Value / 1MB, 2))Mi" } Else { [string]$Value }
                                    $Row[$Metric]            = $DisplayValue
                                    $Row["${Metric}Percent"] = $Percent
                                }
                            [pscustomobject]$Row
                        }

                        $Labels      = [ordered]@{}
                        $Annotations = [ordered]@{}
                        If($NodeItem.Metadata.Labels)
                            {
                                ForEach($Key in $NodeItem.Metadata.Labels.Keys)
                                    {
                                        $Labels[$Key] = $NodeItem.Metadata.Labels[$Key]
                                    }
                            }
                        If($NodeItem.Metadata.Annotations)
                            {
                                ForEach($Key in $NodeItem.Metadata.Annotations.Keys)
                                    {
                                        $Annotations[$Key] = $NodeItem.Metadata.Annotations[$Key]
                                    }
                            }

                        $Addresses = [ordered]@{}
                        ForEach($Address in $NodeItem.Status.Addresses)
                            {
                                $Addresses[$Address.Type] = $Address.Address
                            }

                        $Conditions = @($NodeItem.Status.Conditions | ForEach-Object {
                            [pscustomobject]@{
                                Type               = $_.Type
                                Status             = $_.Status
                                LastHeartbeatTime  = $_.LastHeartbeatTime
                                LastTransitionTime = $_.LastTransitionTime
                                Reason             = $_.Reason
                                Message            = $_.Message
                            }
                        })

                        $SystemInfo = [ordered]@{
                            MachineID               = $NodeItem.Status.NodeInfo.MachineID
                            SystemUUID              = $NodeItem.Status.NodeInfo.SystemUUID
                            BootID                  = $NodeItem.Status.NodeInfo.BootID
                            KernelVersion           = $NodeItem.Status.NodeInfo.KernelVersion
                            OSImage                 = $NodeItem.Status.NodeInfo.OsImage
                            ContainerRuntimeVersion = $NodeItem.Status.NodeInfo.ContainerRuntimeVersion
                            KubeletVersion          = $NodeItem.Status.NodeInfo.KubeletVersion
                            KubeProxyVersion        = $NodeItem.Status.NodeInfo.KubeProxyVersion
                            OperatingSystem         = $NodeItem.Status.NodeInfo.OperatingSystem
                            Architecture            = $NodeItem.Status.NodeInfo.Architecture
                        }
                        $Lease = [ordered]@{}
                        Try
                            {
                                $LeaseItem = [k8s.CoordinationV1OperationsExtensions]::ReadNamespacedLeaseAsync(
                                    $Script:KubePSDirectClient.CoordinationV1,
                                    $NodeName,
                                    'kube-node-lease'
                                ).GetAwaiter().GetResult()

                                $Lease = [ordered]@{
                                    HolderIdentity       = $LeaseItem.Spec.HolderIdentity
                                    LeaseDurationSeconds = $LeaseItem.Spec.LeaseDurationSeconds
                                    AcquireTime          = $LeaseItem.Spec.AcquireTime
                                    RenewTime            = $LeaseItem.Spec.RenewTime
                                    LeaseTransitions     = $LeaseItem.Spec.LeaseTransitions
                                }
                            }
                        Catch [k8s.Autorest.HttpOperationException]
                            {
                                If($_.Exception.Response.StatusCode -ne [System.Net.HttpStatusCode]::NotFound) { Throw }
                            }
                        $Events = [k8s.CoreV1OperationsExtensions]::ListEventForAllNamespacesAsync(
                            $Script:KubePSDirectClient.CoreV1,
                            $null,
                            $null,
                            "involvedObject.name=$NodeName,involvedObject.kind=Node"
                        ).GetAwaiter().GetResult().Items |
                            Where-Object { $_.InvolvedObject.Name -eq $NodeName -and $_.InvolvedObject.Kind -eq 'Node' } |
                            ForEach-Object {
                                $EventTime = If($_.LastTimestamp) { $_.LastTimestamp } ElseIf($_.EventTime) { $_.EventTime } Else { $_.Metadata.CreationTimestamp }
                                [pscustomobject]@{
                                    Type    = $_.Type
                                    Reason  = $_.Reason
                                    Age     = Resolve-KubeResourceAge -Timestamp ([string]$EventTime)
                                    From    = $_.Source.Component
                                    Message = $_.Message
                                }
                            }

                        [pscustomobject]@{
                            PSTypeName         = 'KubePS.NodeDescription'
                            Name               = $NodeItem.Metadata.Name
                            Roles              = (@($Labels.Keys | Where-Object { $_ -like 'node-role.kubernetes.io/*' } | ForEach-Object { $_.Substring('node-role.kubernetes.io/'.Length) }) -join ',')
                            Labels             = $Labels
                            Annotations        = $Annotations
                            CreationTimestamp  = $NodeItem.Metadata.CreationTimestamp
                            Taints             = @($NodeItem.Spec.Taints | ForEach-Object { "$($_.Key)=$($_.Value):$($_.Effect)" }) -join ','
                            Unschedulable      = [bool]$NodeItem.Spec.Unschedulable
                            Addresses          = $Addresses
                            Lease              = $Lease
                            Conditions         = $Conditions
                            Capacity           = $Capacity
                            Allocatable        = $Allocatable
                            SystemInfo         = $SystemInfo
                            NonTerminatedPods  = @($NonTerminatedPods)
                            AllocatedResources = @($AllocatedResources)
                            Events             = @($Events)
                            Sections           = [ordered]@{}
                        }
                    }
            }
    }