Microsoft.PowerShell.KubeCtl.psm1
# a couple of module wide resources $TextInfo = [CultureInfo]::new("en-us",$false).TextInfo $Executable = "kubectl" class Readiness { [int]$Count [int]$Ready [string]ToString() { return ("{0}/{1}" -f $this.ready, $this.count) } Readiness([string]$r) { $this.,$this.count = $r.Split("/") } Readiness([int]$c, [int]$r) { $this.count = $c $this.Ready = $r } } class Deployment { hidden [pscustomobject]$OriginalObject [string]$Namespace [string]$Name [Readiness]$Readiness [int]$Updated [int]$Available [DateTime]$StartDate Deployment ( [pscustomobject]$o ) { $this.OriginalObject = $o $this.Namespace = $o.metadata.namespace $this.Name = $o.metadata.name $this.Readiness = [Readiness]::new($o.status.replicas,$o.status.readyreplicas) $this.Updated = $o.status.updatedReplicas $this.Available = $o.status.availableReplicas $this.StartDate = $o.metadata.creationTimestamp } } function Get-ClassDefinition ( [psobject]$configuration, [ref]$className ) { $outputType = $configuration.TypeName $className.Value = $outputType $sb = [System.Text.StringBuilder]::new() $null = $sb.AppendLine('class ' + $outputType + " {") $null = $sb.AppendLine('# fields') $null = $configuration.Fields.Foreach({$sb.AppendLine(' [object]$' + $textinfo.ToTitleCase($_.PropertyName.ToLower().Replace(" ","").Replace("-","")))}) $null = $sb.AppendLine(' hidden [psobject]$originalObject') $null = $sb.AppendLine('# originalObject member') $null = $sb.AppendLine("") $null = $sb.AppendLine('# constructor') $null = $sb.AppendLine(" $outputType ([pscustomobject]`$o) {") $null = $sb.AppendLine(' if ( $env:DebugAutoConstructor -eq $true ) {') $null = $sb.AppendLine(' wait-debugger') $null = $sb.AppendLine(' }') $null = $configuration.Fields.Foreach({$sb.AppendLine(' $this.' + $textinfo.ToTitleCase($_.PropertyName.ToLower().Replace(" ","").Replace("-","")) + ' = ' + $_.PropertyReference)}) $null = $sb.AppendLine(' $this.originalObject = $o') $null = $sb.AppendLine(' }') $null = $sb.AppendLine('}') return $sb.ToString() } function Get-ResourceTypes ( [string]$resourceJson = "${psscriptroot}/ResourceConfiguration.json") { $script:classStrings = @() $resources = Get-Content $resourceJson | ConvertFrom-Json foreach ($resourceType in $resources) { [ref]$className = "" $classdef = Get-ClassDefinition -configuration $resourceType -className $className # create the classes only if they're not already available if ( ! ("$className" -as [type]) ) { $script:classStrings += $classdef } } # this instantiates all the types that will be needed return ($script:classStrings -join "`n`n") } function Initialize-Formatters ( [string]$resourceJson = "${psscriptroot}/ResourceConfiguration.json") { $resources = Get-Content $resourceJson | ConvertFrom-Json $generatedFormatFile = "${PSScriptRoot}/Generated.Format.ps1xml" .{ '<Configuration>' ' <ViewDefinitions>' foreach ( $resource in $resources ) { ' <View>' ' <Name>{0}Table</Name>' -f $resource.TypeName ' <ViewSelectedBy>' ' <TypeName>{0}</TypeName>' -f $resource.TypeName ' </ViewSelectedBy>' ' <TableControl>' ' <TableHeaders>' $resource.Fields.Foreach({' <TableColumnHeader><Label>{0}</Label></TableColumnHeader>' -f $textInfo.ToTitleCase($_.PropertyName.ToLower().Replace(" ","").Replace("-",""))}) ' </TableHeaders>' ' <TableRowEntries>' ' <TableRowEntry>' ' <TableColumnItems>' $resource.Fields.Foreach({' <TableColumnItem><PropertyName>{0}</PropertyName></TableColumnItem>' -f $textInfo.ToTitleCase($_.PropertyName.ToLower().Replace(" ","").Replace("-",""))}) ' </TableColumnItems>' ' </TableRowEntry>' ' </TableRowEntries>' ' </TableControl>' ' </View>' } ' </ViewDefinitions>' '</Configuration>' } > $generatedFormatFile $generatedFormatFile } function Get-KubeDeployment { param ( $name = "*" ) $items = Invoke-KubeCtl -verb get -resource deployment $items.ForEach({[deployment]::new($_)}).Where({$_.name -like "$name"}) } $proxyFunctions = @{ "get:deployments" = { [CmdletBinding()] param ($name = "*") $items = Invoke-KubeCtl -verb get -resource deployment $items.ForEach({[deployment]::new($_)}).Where({$_.name -like "$name"}) } "get:pods" = { [CmdletBinding()] param ( $name = "*" ) $items = Invoke-KubeCtl -verb get -resource pod $items.foreach({[Pod]::new($_)}).Where({$_.Name -like $name}) } "get:endpoints" = { [CmdletBinding()] param ($name = "*") $items = Invoke-KubeCtl -verb get -resource endpoints $items.ForEach({[endpoints]::new($_)}).Where({$_.name -like "$name"}) } "get:events" = { [CmdletBinding()] param ($name = "*") $items = Invoke-KubeCtl -verb get -resource events $items.ForEach({[events]::new($_)}).Where({$_.name -like "$name"}) } } # NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES # kube-system helm-install-traefik-z4j9n 0/1 Completed 1 2d18h 10.42.0.3 jwtraspbian04 <none> <none> class Pod { [string]$NameSpace [string]$Name [Readiness]$Ready [string]$Status [int]$Restarts [DateTime]$StartDate [URI]$Ip [string]$NodeName [string]$NominatedNode [string]$ReadinessGates hidden [pscustomobject]$OriginalObject Pod ([pscustomobject]$o ) { $this.OriginalObject = $o $this.Namespace = $o.metadata.namespace $this.name = $o.metadata.name $this.StartDate = $o.metadata.creationTimestamp $this.NodeName = $o.spec.nodeName $this.Ip = $o.status.podip $this.Restarts = ($o.status.containerStatuses.restartcount|measure-object -sum ).sum [int]$totalCount = $o.Status.containerStatuses.Count [int]$readyCount = $o.status.ContainerStatuses.State.running.Count $this.Ready = [Readiness]::new($totalCount, $readyCount) $this.Status = $o.status.phase } } function Get-KubePod2 { param ( $name = ".*" ) $items = (& ${executable} get pods --all-namespaces -o json | ConvertFrom-Json).Items $items.foreach({[Pod]::new($_)}).Where({$_.Name -match $name}) } class KubeResource { [string]$Name [string[]]$Shortnames [string]$ApiGroup [bool]$Namespaced [string]$Kind [string[]]$Verbs KubeResource([int[]]$offsets, $string) { $this.name = $string.substring($offsets[0],($offsets[1]-1)).Trim() $this.Shortnames = $string.substring($offsets[1],($offsets[2]-$offsets[1])).Replace(" ","").Split(",") $this.ApiGroup = $string.substring($offsets[2],($offsets[3]-$offsets[2])).Trim() $this.Namespaced = [bool]::Parse($string.substring($offsets[3],($offsets[4]-$offsets[3])).Trim()) $this.Kind = $string.substring($offsets[4],($offsets[5]-$offsets[4])).Trim() $this.Verbs = $string.substring($offsets[5]).Replace("[","").Replace("]","").Split(" ") } [string]ToString() { return $this.Name } } $KUBERESOURCES = $null if ( $global:DEFAULTSESSION ) { $DEFAULTSESSION = $global:DEFAULTSESSION } else { $DEFAULTSESSION = $null } if ( $global:DefaultRequireSudo ) { $DefaultRequireSudo = $global:DefaultRequireSudo } else { $DefaultRequireSudo = $false } <# .SYNOPSIS Set the session for executing kubectl .DESCRIPTION Do not use, untested #> function Set-DefaultPSSession { [CmdletBinding(SupportsShouldProcess=$true)] param ( [System.Management.Automation.Runspaces.PSSession]$Session ) if ( $PSCmdlet.ShouldProcess("session")) { $script:DEFAULTSESSION = $Session } } <# .SYNOPSIS Get the session for executing kubectl .DESCRIPTION Do not use, untested #> function Get-DefaultPSSession { [CmdletBinding()] param () return $script:DEFAULTSESSION } <# .SYNOPSIS Get whether invoking kubectl requries sudo .DESCRIPTION Do not use, it is not cross platform yet #> function Get-KubeRequireSudo { [CmdletBinding()] param () return $script:DefaultRequireSudo } <# .SYNOPSIS Set whether invoking kubectl requries sudo .DESCRIPTION Do not use, it is not cross platform yet #> function Set-KubeRequireSudo { [CmdletBinding(SupportsShouldProcess=$true)] param ( [bool]$RequireSudo ) if ( $PSCmdlet.ShouldProcess("require sudo")) { $script:DefaultRequireSudo = $RequireSudo } } <# .SYNOPSIS Retrieve the available api-resources .DESCRIPTION This converts the table output of kubectl api-resources to a set of objects which will then be used to create the proxy functions #> function Get-KubeResource { [CmdletBinding()] param ( [string]$name = ".*", [switch]$Force ) # kubectl api-resources -o wide # We do a little caching here, -force will ensure we go back and collect resources if ( ! $script:KUBERESOURCES -or $Force) { $res = Invoke-KubeCtl -verb "" -resource "api-resources" -noJson -arguments @("-o","wide") -noAllNamespace $FIELDS = "NAME","SHORTNAMES","APIGROUP","NAMESPACED","KIND","VERBS" $offsets = $FIELDS.ForEach({$res[0].IndexOf("$_")}) $script:KUBERESOURCES = $res[1..($res.count-1)].Foreach({[KubeResource]::new($offsets,$_)}).Where({$_.name -match $name}) } return $script:KUBERESOURCES.Where({$_.name -match $name}) } <# .SYNOPSIS Create the proxy functions for the module .DESCRIPTION This discovers the available resources and creates a proxy function that you can call It essentially converts 'kubectl get pod' to 'Get-KubePod' #> function Initialize-ProxyFunction { $r = Get-KubeResource export-modulemember -Function 'Invoke-KubeCtl', 'Get-KubeResource', 'Initialize-ProxyFunction', 'Get-DefaultPSSession', 'Set-DefaultPSSession', 'Get-KubeRequireSudo', 'Set-KubeRequireSudo' # for each resource that has a get verb, create a function # which can retrieve and display it. # in the case that there is a provided implementation, use it # otherwise create default which has a name parameter # This should probably include a namespace parameter where the resource supports namespaces $getters = $r.where({ $_.verbs -contains "get" }) $getters.foreach({ $resource = $_.Name $kind = $_.kind $proxyKey = "get:{0}" -f $resource $implementation = $proxyFunctions[$proxyKey] $functionName = "Get-Kube${kind}" if ( $implementation ) { [scriptblock]::Create("function global:${functionName} { $implementation }").Invoke() } elseif ($resource -as [type]) { [scriptBlock]::Create("function global:$functionName { [CmdletBinding()] param ([string]`$Name = `"*`") (Invoke-KubeCtl -Verb get -resource $resource).Foreach({[$resource]::new(`$_)}).Where({`$_.Name -like `"`$Name`"}) }").Invoke() } else { [scriptBlock]::Create("function global:$functionName { [CmdletBinding()] param () Invoke-KubeCtl -Verb get -resource $resource }").Invoke() } Export-ModuleMember -Function $functionName }) } <# .SYNOPSIS Invoke kubectl with arguments .DESCRIPTION Invoke kubectl with arguments #> function Invoke-KubeCtl { [CmdletBinding()] param ( [switch]$requireSudo, [System.Management.Automation.Runspaces.PSSession]$session = $script:DEFAULTSESSION, [string]$verb, [string]$resource, [switch]$noJson, [switch]$noAllNamespace, [string[]]$arguments ) [string[]]$action = @() if ( $requireSudo -or $script:DefaultRequireSudo) { $action += "sudo ${executable}" } else { $action += "${executable}" } $action += $verb $action += $resource if ( ! $noAllNamespace ) { $action += "--all-namespaces" } if ( ! $noJson ) { $action += "-o","json" } $action += $arguments # always multiplex the error stream to be sure that we get them # we will de-multiplex them after the execution. $action += '2>&1' # create the script block to execute [scriptblock]$action = [scriptblock]::create($action) Write-Debug -Message ("SESSION IS NULL: {0}" -f $null -eq $script:DEFAULTSESSION) Write-Debug -Message $action.ToString() if ( $session ) { $allOutput = invoke-command -session $session -scriptblock $action } else { $allOutput = & $action } $execErrors = $allOutput | Where-Object { $_ -is [System.Management.Automation.ErrorRecord]} $result = $allOutput | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord]} # should this throw? if ( $execErrors ) { $execErrors | Write-Error } if ( ! $noJson ) { $convertedResult = $result | ConvertFrom-Json $convertedResult.Items } else { return $result } } # retrieve the resource types as PowerShell classes # and add them and make them available to the module $classStr = Get-ResourceTypes $sb = [scriptblock]::create($classStr) . $sb # add the formatters and add them to the module $generatedFormatFile = Initialize-Formatters update-formatdata $generatedFormatFile # generate the proxy functions Initialize-ProxyFunction |