functions/k8s.ps1

<#PSScriptInfo
.VERSION 1.1.0
.GUID bfaf255e-8e7b-4354-824f-46ac9625cab5
.AUTHOR Code Dx
.DESCRIPTION Includes Kubernetes-related helpers
#>


function New-Namespace([string] $namespace, [Tuple`2[string,string]] $label, [switch] $dryRun) {

    if (-not $dryRun) {
        if (Test-Namespace $namespace) {
            Set-NamespaceLabel $namespace $label.Item1 $label.Item2
            return [string]::Empty
        }
    }

    $output = $dryRun ? 'yaml' : 'name'
    $dryRunParam = $dryRun ? (Get-KubectlDryRunParam) : ''

    $newNamespace = kubectl create namespace $namespace -o $output $dryRunParam
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to create namespace $namespace, kubectl exited with code $LASTEXITCODE."
    }

    if ($null -eq $label) {
        return $newNamespace
    }

    if (-not $dryRun) {
        Set-NamespaceLabel $namespace $label.Item1 $label.Item2
        return $newNamespace
    }

    # create namespace doesn't currently have a label parameter, so add label to YAML
    $newNamespaceWithLabel = @()
    $newNamespace | ForEach-Object {
        $newNamespaceWithLabel += $_
        if ($_ -eq 'metadata:') {
            $newNamespaceWithLabel += ' labels:'
            $newNamespaceWithLabel += " $($label.Item1): $($label.Item2)"
        }
    }
    $newNamespaceWithLabel
}

function Test-Namespace([string] $namespace) {

    if ('' -eq $namespace) {
        return $false
    }
    
    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl get namespace $namespace *>&1 | out-null
    0 -eq $LASTEXITCODE
}

function Get-KubectlVersion {

    $Local:ErrorActionPreference = 'Continue'
    $version = kubectl version -o json
    if (0 -ne $LASTEXITCODE) {
        throw "Unable to run 'kubectl version' command, kubectl exited with exit code $LASTEXITCODE. Is your environment connected to your cluster?"
    }
    $version | ConvertFrom-Json
}

function Get-KubectlClientVersion {

    $versionInfo = Get-KubectlVersion
    $version = Get-SemanticVersionComponents $versionInfo.clientVersion.gitVersion
    "$($version[1]).$($version[2])"
}

function Get-KubectlServerSemanticVersion {

    $versionInfo = Get-KubectlVersion
    Get-SemanticVersionComponents $versionInfo.serverVersion.gitVersion
}

function Get-KubectlServerVersion {

    $version = Get-KubectlServerSemanticVersion
    "$($version[1]).$($version[2])"
}

function Get-KubectlServerVersionNumber {
    [float]::Parse((Get-KubectlServerVersion))
}

function Get-KubectlServerVersionMajor {
    [int]::Parse(((Get-KubectlServerSemanticVersion)[1]))
}

function Get-KubectlServerVersionMinor {
    [int]::Parse(((Get-KubectlServerSemanticVersion)[2]))
}

function Get-KubectlContext() {

    $Local:ErrorActionPreference = 'Continue'
    $contextName = kubectl config current-context
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to get kubectl context, kubectl exited with code $LASTEXITCODE."
    }
    $contextName
}

function Get-KubectlContexts([switch] $nameOnly) {

    $output = @()
    if ($nameOnly) {
        $output = '-o','name'
    }
    $contexts = kubectl config get-contexts @output
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to get kubectl contexts, kubectl exited with code $LASTEXITCODE."
    }
    $contexts
}

function Get-KubernetesPort() {

    $info = kubectl cluster-info
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to get kubectl cluster info, kubectl exited with code $LASTEXITCODE."
    }

    $urlMatch = $info[0] | select-string '(?<url>http[A-Z0-9a-z:/\.\-]+)'
    if (-not $urlMatch.Matches.Success) {
        throw "Expected to find URL on line 1: $($info[0])."
    }
    $url = $urlMatch.Matches.Groups[1].Value
    ([Uri]$url).Port
}

function Get-KubernetesEndpointsPort() {

    $json = kubectl get endpoints/kubernetes -o json | convertfrom-json
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to get kubernetes endpoints, kubectl exited with code $LASTEXITCODE."
    }

    $port = $null
    $portInfo = $json.subsets.ports | select-object -First 1
    if ($null -ne $portInfo) {
        $port = $portInfo | select-object -ExpandProperty port
    }
    $port
}

function Set-KubectlContext([string] $contextName) {

    $Local:ErrorActionPreference = 'Continue'
    kubectl config use-context $contextName *>&1 | out-null
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to change kubectl context, kubectl exited with code $LASTEXITCODE."
    }
}

function Test-CurrentKubeContext() {

    # Test-CurrentKubeContext will return false if the caller has no current context (e.g., kubectl config unset current-context)
    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl config current-context *>&1 | out-null
    0 -eq $LASTEXITCODE
}

function Test-ClusterInfo([string] $profileName) {

    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl cluster-info *>&1 | out-null
    0 -eq $LASTEXITCODE
}

function New-Certificate([string] $csrSignerName, [string] $caCertPath, [string] $resourceName, [string] $dnsName, [string] $certPublicKeyFile, [string] $certPrivateKeyFile, [string] $namespace, [string[]] $alternativeNames){

    $altNames = @()
    $altNames = $altNames + "$dnsName.$namespace" + "$dnsName.$namespace.svc.cluster.local" + $alternativeNames

    New-Csr $dnsName `
        $altNames `
        "$dnsName.conf" `
        "$dnsName.csr" `
        $certPrivateKeyFile

    $attempt = 0
    while ($true) {
        try {
            $attempt++

            New-CsrResource $csrSignerName $resourceName "$dnsName.csr" "$dnsName.csrr" $namespace
            New-CsrApproval $resourceName
    
            $certText = Get-CertificateFromCsr $resourceName
            $caCertText = [io.file]::ReadAllText($caCertPath)
            "$certText`n$caCertText" | out-file $certPublicKeyFile -Encoding ascii -Force

            break
        } catch {

            if ($attempt -gt 3) {
                throw $_
            }
            Write-Verbose "Error: $_`n`nRetrying certificate request..."
            Start-Sleep 60s
        }
    }
}

function New-Csr([string] $subjectName, [string[]] $subjectAlternativeNames, [string] $requestFile, [string] $csrFile, [string] $keyFile) {

    $request = @'
[ req ]
default_bits = 2048
prompt = no
encrypt_key = no
distinguished_name = req_dn
req_extensions = req_ext

[ req_dn ]
CN = {0}

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = {0}
'@
 -f $subjectName

$i = 2
$subjectAlternativeNames | ForEach-Object {
    $request = $request + ("`nDNS.{0} = {1}" -f $i,$_)
    $i += 1
}

    $request | out-file $requestFile -Encoding ascii -Force

    # Note: When using EKS, this requires k8s v1.16 or newer. Older EKS releases do not support subject alternative names.
    openssl req -new -config $requestFile -out $csrFile -keyout $keyFile
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to create CSR, openssl exited with code $LASTEXITCODE."
    }
}

function Test-Deployment([string] $namespace, [string] $deploymentName) {

    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl -n $namespace get deployment $deploymentName *>&1 | out-null
    $LASTEXITCODE -eq 0
}

function Test-NonNamespacedResource([string] $kind, [string] $resourceName) {
    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl get $kind $resourceName *>&1 | out-null
    $LASTEXITCODE -eq 0
}

function Test-NamespacedResource([string] $namespace, [string] $kind, [string] $resourceName) {
    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl -n $namespace get $kind $resourceName *>&1 | out-null
    $LASTEXITCODE -eq 0
}

function Test-StatefulSet([string] $namespace, [string] $statefulSetName) {

    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl -n $namespace get statefulset $statefulSetName *>&1 | out-null
    $LASTEXITCODE -eq 0
}

function Test-CsrResource([string] $resourceName) {

    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl get csr $resourceName *>&1 | out-null
    $LASTEXITCODE -eq 0
}

function Remove-CsrResource([string] $resourceName) {

    kubectl delete csr $resourceName
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to delete CSR, kubectl exited with code $LASTEXITCODE."
    }
}

function Get-CsrSignerNameLegacyUnknown {
    'kubernetes.io/legacy-unknown'
}

function New-CsrResource([string] $csrSignerName, [string] $resourceName, [string] $csrFile, [string] $csrResourceFile) {

    $csrSignerNameLegacyUnknown = Get-CsrSignerNameLegacyUnknown
    $isBetaCsrRequired = $csrSignerName -eq $csrSignerNameLegacyUnknown

    $apiVersion = 'certificates.k8s.io/v1'
    if ($isBetaCsrRequired) {

        if (-not (Test-CertificateSigningRequestV1Beta1)) {
            throw "CSR signerName $csrSignerName is invalid because $csrSignerNameLegacyUnknown requires the CSR API version v1beta1"
        }

        $apiVersion = 'certificates.k8s.io/v1beta1'
    }

    if (Test-CsrResource $resourceName) {
        Remove-CsrResource $resourceName
    }


    $resourceRequest = @'
apiVersion: {0}
kind: CertificateSigningRequest
metadata:
  name: {1}
spec:
  groups:
  - system:authenticated
  request: {2}
  signerName: {3}
  usages:
  - digital signature
  - key encipherment
  - server auth
'@
 -f $apiVersion, $resourceName, (Convert-Base64 $csrFile), $csrSignerName

    $resourceRequest | out-file  $csrResourceFile -Encoding ascii -Force

    kubectl create -f $csrResourceFile
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to apply CSR resource, kubectl exited with code $LASTEXITCODE."
    }
}

function New-CsrApproval([string] $resourceName) {

    kubectl certificate approve $resourceName
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to approve CSR, kubectl exited with code $LASTEXITCODE."
    }
}

function Get-CertificateFromCsr([string] $resourceName, [int] $waitSeconds=120) {

    $sleepSeconds = [math]::min(60, ($waitSeconds * .05))
    $timeoutTime = [datetime]::Now.AddSeconds($waitSeconds)
    while ($true) {

        $certData = kubectl get csr $resourceName -o jsonpath='{.status.certificate}'
        if ($LASTEXITCODE -ne 0) {
            throw "Unable to retrieve certificate from CSR, kubectl exited with code $LASTEXITCODE."
        }

        if ($null -ne $certData) {

            $certBytes = [convert]::frombase64string($certData)
            [text.encoding]::ascii.getstring($certBytes)
            break
        }

        if ([datetime]::now -gt $timeoutTime) {
            throw "Unable to continue because the certificate $resourceName is not ready"
        }

        Write-Verbose "Certificate $resourceName is not available. Another check will occur in $sleepSeconds seconds."
        start-sleep -seconds $sleepSeconds
    }
}

function Set-NamespaceLabel([string] $namespace, [string] $labelName, [string] $labelValue) {

    kubectl label namespace $namespace $labelName`=$labelValue --overwrite
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to create $namespace label with $labelName=$labelValue, kubectl exited with code $LASTEXITCODE."
    }
}

function Test-Service([string] $namespace, [string] $name) {

    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl -n $namespace get svc $name *>&1 | out-null
    0 -eq $LASTEXITCODE
}

function Test-Secret([string] $namespace, [string] $name) {

    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl -n $namespace get secret $name *>&1 | out-null
    0 -eq $LASTEXITCODE
}

function Get-SecretFieldValue([string] $namespace, [string] $name, [string] $field) {

    if (-not (Test-Secret $namespace $name)) {
        return $null
    }
    $val = kubectl -n $namespace get secret $name -o jsonpath="{.data.$field}"
    if ($null -eq $val) {
        return $null
    }
    [text.encoding]::ASCII.GetString([convert]::FromBase64String($val))
}

function Remove-Secret([string] $namespace, [string] $name) {

    $Local:ErrorActionPreference = 'Continue'
    kubectl -n $namespace delete secret $name *>&1 | out-null
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to delete secret named $name, kubectl exited with code $LASTEXITCODE."
    }
}

function New-GenericSecret([string] $namespace, [string] $name, [hashtable] $keyValues = @{}, [hashtable] $fileKeyValues = @{}, [switch] $dryRun) {
    
    if (-not $dryRun) {
        if (Test-Secret $namespace $name) {
            Remove-Secret $namespace $name
        }
    }

    $pairs = @()
    $tmpPaths = @()
    try {
        $keyValues.Keys | ForEach-Object {

            # apply escape required when running from pwsh
            $value = $keyValues[$_]
            $value = $value -replace '"','\"'

            $pairs += "--from-literal=$_=$value"
        }
        $fileKeyValues.Keys | ForEach-Object {
            $fromFilePath = Set-KubectlFromFilePath $fileKeyValues[$_] ([ref]$tmpPaths)
            $pairs += "--from-file=$_=$fromFilePath"
        }
        if ($pairs.Length -eq 0) {
            throw "Unable to create secret named $name with no data."
        }

        $output = $dryRun ? 'yaml' : 'name'
        $dryRunParam = $dryRun ? (Get-KubectlDryRunParam) : ''
        kubectl -n $namespace create secret generic $name $pairs -o $output $dryRunParam
        if ($LASTEXITCODE -ne 0) {
            throw "Unable to create secret named $name, kubectl exited with code $LASTEXITCODE."
        }
    } finally {
        $tmpPaths | ForEach-Object { Write-Verbose "Removing temporary file '$_'"; Remove-Item $_ -Force }
    }
}

function New-CertificateSecret([string] $namespace, [string] $name, [string] $certFile, [string] $keyFile, [switch] $dryRun) {

    if (-not $dryRun) {
        if (Test-Secret $namespace $name) {
            Remove-Secret $namespace $name
        }
    }

    $output = $dryRun ? 'yaml' : 'name'
    $dryRunParam = $dryRun ? (Get-KubectlDryRunParam) : ''

    kubectl -n $namespace create secret tls $name --cert`=$certFile --key`=$keyFile -o $output $dryRunParam
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to create secret named $name, kubectl exited with code $LASTEXITCODE."
    }
}

function Test-ConfigMap([string] $namespace, [string] $name) {

    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl -n $namespace get configmap $name *>&1 | out-null
    0 -eq $LASTEXITCODE
}

function Remove-ConfigMap([string] $namespace, [string] $name) {

    $Local:ErrorActionPreference = 'Continue'
    kubectl -n $namespace delete configmap $name *>&1 | out-null
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to delete configmap named $name, kubectl exited with code $LASTEXITCODE."
    }
}

function New-CertificateConfigMap([string] $namespace, [string] $name, [string] $certFile, [string] $certFilenameInConfigMap, [switch] $dryRun) {
    if ('' -eq $certFilenameInConfigMap) {
        $certFilenameInConfigMap = split-path $certFile -Leaf
    }
    New-ConfigMap $namespace $name @{} @{$certFilenameInConfigMap = $certFile} -dryRun:$dryRun
}

function Set-KubectlFromFilePath([string] $fromFilePath, [ref] $tmpPaths) {
    if ($fromFilePath.Contains(',')) {
        $newFromFilePath = New-TemporaryFile
        $tmpPaths.Value += $newFromFilePath.FullName

        Write-Verbose "Created file '$($newFromFilePath.FullName)' for file '$fromFilePath'"
        Copy-Item -LiteralPath $fromFilePath -Destination $newFromFilePath.FullName

        $fromFilePath = $newFromFilePath.FullName
    }
    $fromFilePath
}

function New-ConfigMap([string] $namespace, [string] $name, [hashtable] $keyValues = @{}, [hashtable] $fileKeyValues = @{}, [switch] $dryRun) {
    
    if (-not $dryRun) {
        if (Test-ConfigMap $namespace $name) {
            Remove-ConfigMap $namespace $name
        }
    }

    $pairs = @()
    $tmpPaths = @()
    try {
        $keyValues.Keys | ForEach-Object {

            # apply escape required when running from pwsh
            $value = $keyValues[$_]
            $value = $value -replace '"','\"'

            $pairs += "--from-literal=$_=$value"
        }
        $fileKeyValues.Keys | ForEach-Object {
            $fromFilePath = Set-KubectlFromFilePath $fileKeyValues[$_] ([ref]$tmpPaths)
            $pairs += "--from-file=$_=$fromFilePath"
        }
        if ($pairs.Length -eq 0) {
            throw "Unable to create configmap named $name with no data."
        }

        $output = $dryRun ? 'yaml' : 'name'
        $dryRunParam = $dryRun ? (Get-KubectlDryRunParam) : ''
        kubectl -n $namespace create configmap $name $pairs -o $output $dryRunParam
        if ($LASTEXITCODE -ne 0) {
            throw "Unable to create configmap named $name, kubectl exited with code $LASTEXITCODE."
        }
    } finally {
        $tmpPaths | ForEach-Object { Write-Verbose "Removing temporary file '$_'"; Remove-Item $_ -Force }
    }
}


function Set-NonNamespacedResource([string] $jsonPath, [string] $kind, [switch] $dryRun) {

    $resourceName = (get-content $jsonPath | ConvertFrom-Json).metadata.name
    if (-not $dryRun -and (Test-NonNamespacedResource $kind $resourceName)) {
        kubectl replace -f $jsonPath -o 'name' # Do not use apply here because it may fail with a resourceVersion mismatch
        return
    }

    $output = $dryRun ? 'yaml' : 'name'
    $dryRunParam = $dryRun ? (Get-KubectlDryRunParam) : ''
    kubectl apply -f $jsonPath -o $output $dryRunParam
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to create non namespaced resource from $jsonPath, kubectl exited with code $LASTEXITCODE."
    }
}

function Set-K8sResource([string] $path) {

    $Local:ErrorActionPreference = 'Continue'
    kubectl apply -f $path *>&1 | out-null
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to apply resource from $path, kubectl exited with code $LASTEXITCODE."
    }
}

function New-NamespacedResource([string] $namespace, [string] $kind, [string] $resourceName, [string] $yamlPath) {

    if (Test-NamespacedResource $namespace $kind $resourceName) {
        Remove-NamespacedResource $namespace $kind $resourceName
    }

    kubectl -n $namespace apply -f $yamlPath -o 'name'
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to create $kind resource from $yamlPath, kubectl exited with code $LASTEXITCODE."
    }
}

function New-ImagePullSecret([string] $namespace, 
    [string] $name, 
    [string] $dockerRegistry,
    [string] $dockerRegistryUser,
    [string] $dockerRegistryPwd,
    [switch] $dryRun) {

    if (-not $dryRun) {
        if (Test-Secret $namespace $name) {
            Remove-Secret $namespace $name
        }
    }

    $output = $dryRun ? 'yaml' : 'name'
    $dryRunParam = $dryRun ? (Get-KubectlDryRunParam) : ''

    kubectl -n $namespace create secret docker-registry $name --docker-server=$dockerRegistry --docker-username=$dockerRegistryUser --docker-password=$dockerRegistryPwd -o $output $dryRunParam
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to create image pull secret named $name, kubectl exited with code $LASTEXITCODE."
    }
}

function Wait-AllRunningPods([string] $message, [int] $waitSeconds, [string] $namespace) {

    $sleepSeconds = [math]::min(60, ($waitSeconds * .05))
    $timeoutTime = [datetime]::Now.AddSeconds($waitSeconds)
    while ($true) {

        Write-Verbose "Checking for pods that are not in a Running status ($message)..."

        if ('' -eq $namespace) {
            kubectl get pod --all-namespaces
        } else {
            kubectl get pod -n $namespace
        }
        
        if ('' -eq $namespace) {
            $results = kubectl get pod --field-selector=status.phase!=Running,status.phase!=Succeeded,status.phase!=Failed --all-namespaces
        } else {
            $results = kubectl get pod --field-selector=status.phase!=Running,status.phase!=Succeeded,status.phase!=Failed -n $namespace
        }
        
        if ($null -eq $results) {
            Write-Verbose "Wait is over with $($timeoutTime.Subtract([datetime]::Now).TotalSeconds) second(s) remaining before timeout"
            Write-Verbose 'All pods show a Running status'
            break
        }

        if ([datetime]::now -gt $timeoutTime) {
            throw "Unable to continue because a timeout occurred while waiting for all pods to be in a ready status. One or more pods have a status other than Running, Succeeded, or Failed. ($message)."
        }
        Write-Verbose "Some pods are not running. Another check will occur in $sleepSeconds seconds ($message)."
        start-sleep -seconds $sleepSeconds
    }
}

function Wait-RunningPod([string] $message, [int] $waitSeconds, [string] $namespace, [string] $podName) {

    $sleepSeconds = [math]::min(60, ($waitSeconds * .05))
    $timeoutTime = [datetime]::Now.AddSeconds($waitSeconds)
    while ($true) {

        $result = kubectl -n $namespace get pod --field-selector=status.phase=Running,metadata.name="$podName"
        
        if ($null -ne $result) {
            Write-Verbose "Wait is over with $($timeoutTime.Subtract([datetime]::Now).TotalSeconds) second(s) remaining before timeout"
            break
        }

        if ([datetime]::now -gt $timeoutTime) {
            throw "Unable to continue because a timeout occurred while waiting for pod $podName to be in a ready status."
        }
        Write-Verbose "Pod $podName is not yet running. Another check will occur in $sleepSeconds seconds ($message)."
        start-sleep -seconds $sleepSeconds
    }
}

function Wait-Deployment([string] $message, [int] $waitSeconds, [string] $namespace, [string] $deploymentName, [string] $totalReplicas) {

    Wait-ReplicasReady $message $waitSeconds $namespace 'deployment' $deploymentName $totalReplicas
}

function Wait-StatefulSet([string] $message, [int] $waitSeconds, [string] $namespace, [string] $statefulSetName, [string] $totalReplicas) {

    Wait-ReplicasReady $message $waitSeconds $namespace 'statefulset' $statefulSetName $totalReplicas
}

function Wait-JobSuccess([string] $message, [int] $waitSeconds, [string] $namespace, [string] $jobName) {

    $sleepSeconds = [math]::min(60, ($waitSeconds * .05))
    $timeoutTime = [datetime]::Now.AddSeconds($waitSeconds)
    while ($true) {

        $success = kubectl -n $namespace get job $jobName -o jsonpath='{.status.succeeded}'
        if ('1' -eq $success) {
            break
        }

        if ([datetime]::now -gt $timeoutTime) {
            throw "Unable to continue because the job '$jobName' has not yet succeeded ($message)"
        }

        Write-Verbose "Job has not yet succeeded. Another check will occur in $sleepSeconds seconds ($message)."
        start-sleep -seconds $sleepSeconds
    }
}

function Wait-ReplicasReady([string] $message, [int] $waitSeconds, [string] $namespace, [string] $resourceType, [string] $resourceName, [string] $totalReplicas) {

    if ($resourceType -ne 'deployment' -and $resourceType -ne 'statefulset') {
        throw "Unable to wait for resource type '$resourceType'"
    }

    $sleepSeconds = [math]::min(60, ($waitSeconds * .05))
    $startTime = [datetime]::Now
    $timeoutTime = $startTime.AddSeconds($waitSeconds)
    while ($true) {

        $resourceExists = ($resourceType -eq 'deployment' -and (Test-Deployment $namespace $resourceName)) -or 
            ($resourceType -eq 'statefulset' -and (Test-StatefulSet $namespace $resourceName))

        if ($resourceExists) {

            Write-Verbose "Fetching status of $resourceType named $resourceName in namespace $namespace..."
            $readyReplicas = kubectl -n $namespace get $resourceType $resourceName -o jsonpath='{.status.readyReplicas}'
            if ($LASTEXITCODE -ne 0) {
                throw "Unable to wait for $resourceType $resourceName, kubectl exited with code $LASTEXITCODE."
            }

            if ($null -eq $readyReplicas) {
                $readyReplicas = 0
            }
            
            Write-Verbose "Found $readyReplicas of $totalReplicas ready"
            if ($totalReplicas -eq $readyReplicas) {
                Write-Verbose "Wait is over with $($timeoutTime.Subtract([datetime]::Now).TotalSeconds) second(s) remaining before timeout"
                break
            }

        } else {
            Write-Verbose "Resource $resourceName in namespace $namespace does not exist"
        }

        if ([datetime]::now -gt $timeoutTime) {
            throw "Unable to continue because the $resourceType $resourceName is not ready ($message)"
        }

        kubectl -n $namespace get pod
        
        $now = [datetime]::Now
        Write-Verbose $message
        Write-Verbose " Current replica count does not yet match desired count."
        Write-Verbose " Elapsed time is $($now.Subtract($startTime).TotalSeconds) seconds."
        Write-Verbose " Wait will timeout in $($timeoutTime.Subtract($now).TotalSeconds) seconds."
        Write-Verbose " Another replica count check will occur in $sleepSeconds seconds."
        start-sleep -seconds $sleepSeconds
    }
}

function Test-Pod([string] $namespace, [string] $podName) {

    $local:ErrorActionPreference = 'SilentlyContinue'
    kubectl -n $namespace get "pod/$podName" *>&1 | Out-Null
    0 -eq $LASTEXITCODE
}

function Remove-Pod([string] $namespace, [string] $podName, [switch] $force) {

    if (-not (Test-Pod $namespace $podName)) {
        return
    }

    kubectl -n $namespace delete "pod/$podName" ($force ? '--force' : '') ($force ? '--grace-period=0' : '') ($force ? '--wait=false' : '')
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to delete pod/$podName in namespace $namespace, kubectl exited with code $LASTEXITCODE."
    }
}

function Test-PriorityClass([string] $name) {

    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl get priorityclass $name *>&1 | out-null
    $LASTEXITCODE -eq 0
}

function Remove-PriorityClass([string] $name) {

    $Local:ErrorActionPreference = 'Continue'
    kubectl delete priorityclass $name *>&1 | out-null
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to delete priority class named $name, kubectl exited with code $LASTEXITCODE."
    }
}

function New-PriorityClass([string] $name, [int] $value, [switch] $dryRun) {

    if (-not $dryRun) {
        if (Test-PriorityClass $name) {
            Remove-PriorityClass $name
        }
    }

    $output = $dryRun ? 'yaml' : 'name'
    $dryRunParam = $dryRun ? (Get-KubectlDryRunParam) : ''
    kubectl create priorityclass $name --value=$value -o $output $dryRunParam
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to create PriorityClass, kubectl exited with code $LASTEXITCODE."
    }
}

function Format-ResourceLimitRequest([string] $requestMemory, [string] $requestCpu, [string] $requestEphemeralStorage,
    [string] $limitMemory, [string] $limitCpu, [string] $limitEphemeralStorage,
    [int] $indent) {

    $resources = @'
resources:
  requests:
    memory: {0}
    cpu: {1}
    ephemeral-storage: {4}
  limits:
    memory: {2}
    cpu: {3}
    ephemeral-storage: {5}
'@
 -f $requestMemory.trim(),$requestCpu.trim(),
    $limitMemory.trim(),$limitCpu.trim(),
    $requestEphemeralStorage.trim(),$limitEphemeralStorage.trim()

    $resourcesLines = $resources.split("`n") | Where-Object { 
        $_ -notmatch 'memory:\s$' -and $_ -notmatch 'cpu:\s$' -and $_ -notmatch 'ephemeral\-storage:\s$' 
    } | ForEach-Object { 
        "{0}{1}" -f ([string]::new(' ', $indent)),$_ 
    }

    $resourceSpec = [string]::join("`n", $resourcesLines)

    if ($resourceSpec -match '\s+requests:\n\s+limits:') { $resourceSpec = $resourceSpec -replace '\s+requests:','' }
    if ($resourceSpec -notmatch '\s+limits:\n') { $resourceSpec = $resourceSpec -replace '\s+limits:$','' }
    if ($resourceSpec -match '\s*resources:$') { $resourceSpec += ' {}' }

    $resourceSpec
}

function Format-NodeSelector([Tuple`2[string,string][]] $keyValues) {

    if ($null -eq $keyValues) {
        return '{}'
    }

    $items = @{}
    $keyValues | ForEach-Object {
        $items[$_.item1] = $_.item2
    }
    ConvertTo-YamlMap $items
}

function Format-PodTolerationNoScheduleNoExecute([Tuple`2[string,string][]] $keyValues) {

    if ($null -eq $keyValues) {
        return '[]'
    }
    
    $items = @()
    $keyValues | ForEach-Object {

        $toleration = @{}
        $toleration['key'] = $_.item1
        $toleration['value'] = $_.item2
        $toleration['operator'] = 'Equal'
        $toleration['effect'] = 'NoSchedule'
        $items += ConvertTo-YamlMap $toleration

        $toleration['effect'] = 'NoExecute'
        $items += ConvertTo-YamlMap $toleration
    }

    if ($items.count -eq 0) {
        return '[]'
    }
    '[' + "{0}" -f ([string]::Join(',', $items)) + ']'
}

function Set-Replicas([string] $namespace,
    [string] $resourceType,
    [string] $resourceName,
    [int]    $replicaCount,
    [int]    $waitSeconds) {

    kubectl -n $namespace scale --replicas=$replicaCount $resourceType $resourceName
    if (0 -ne $LASTEXITCODE) {
        throw "Unable to set replicas for $resourceType named $resourceName, kubectl exited with code $LASTEXITCODE."
    }

    Wait-ReplicasReady 'Replica Wait' $waitSeconds $namespace $resourceType $resourceName $replicaCount
}

function Get-ServiceAccountName([string] $namespace,
    [string] $resourceType,
    [string] $resourceName) {

    $name = kubectl -n $namespace get $resourceType $resourceName -o jsonpath='{.spec.template.spec.serviceAccountName}'
    if (0 -ne $LASTEXITCODE) {
        throw "Unable to set replicas for $resourceType named $resourceName, kubectl exited with code $LASTEXITCODE."
    }
    $name
}

function Set-DeploymentReplicas([string] $namespace,
    [string] $resourceName,
    [int]    $replicaCount,
    [int]    $waitSeconds) {

    Set-Replicas $namespace 'deployment' $resourceName $replicaCount $waitSeconds
}

function Set-StatefulSetReplicas([string] $namespace,
    [string] $resourceName,
    [int]    $replicaCount,
    [int]    $waitSeconds) {

    Set-Replicas $namespace 'statefulset' $resourceName $replicaCount $waitSeconds
}

function Test-KubernetesJob([string] $namespace,
    [string] $resourceName) {

    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl -n $namespace get job $resourceName *>&1 | out-null
    $LASTEXITCODE -eq 0
}

function Remove-KubernetesJob([string] $namespace,
    [string] $resourceName) {
    
    kubectl delete -n $namespace job $resourceName
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to delete job, kubectl exited with code $LASTEXITCODE."
    }
}

function Remove-KubernetesPvc([string] $namespace,
    [string] $resourceName) {
    
    kubectl delete -n $namespace pvc $resourceName
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to delete pvc, kubectl exited with code $LASTEXITCODE."
    }
}

function Edit-ResourceJsonPath([string] $namespace, [string] $resourceKind, [string] $resourceName, [string] $jsonPatch) {

    kubectl -n $namespace patch $resourceKind $resourceName --type=json -p $jsonPatch
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to edit resource with json patch, kubectl exited with code $LASTEXITCODE."
    }
}

function Edit-ResourceStrategicPatch([string] $namespace, [string] $resourceKind, [string] $resourceName, [string] $patch) {

    kubectl -n $namespace patch $resourceKind $resourceName -p $patch
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to edit resource with strategic patch, kubectl exited with code $LASTEXITCODE."
    }
}

function Add-ResourceLabel([string] $namespace, [string] $resourceKindAndName, [string] $labelKey, [string] $labelValue) {

    if ($namespace -eq '') {
        kubectl label $resourceKindAndName "$labelKey=$labelValue" --overwrite
    } else {
        kubectl -n $namespace label $resourceKindAndName "$labelKey=$labelValue" --overwrite
    }
    
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to add label to resource, kubectl exited with code $LASTEXITCODE."
    }
}

function Remove-ResourceLabel([string] $namespace, [string] $resourceKindAndName, [string] $labelKey) {

    if ($namespace -eq '') {
        kubectl label $resourceKindAndName "$labelKey-"
    } else {
        kubectl -n $namespace label $resourceKindAndName "$labelKey-"
    }
    
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to remove label from resource, kubectl exited with code $LASTEXITCODE."
    }
}

function Remove-NamespacedResource([string] $namespace, [string] $kind, [string] $resourceName) {

    kubectl -n $namespace delete $kind $resourceName
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to delete $resourceName of kind $kind, kubectl exited with code $LASTEXITCODE."
    }
}

function Copy-K8sItem([string] $namespace, 
    [string] $sourcePath,
    [string] $podName,
    [string] $containerName,
    [string] $destinationPath) {

    kubectl -n $namespace cp   -c $containerName $sourcePath $podName`:$destinationPath
    if (0 -ne $LASTEXITCODE) {
        Write-Error "Unable to copy to pod, kubectl exited with exit code $LASTEXITCODE."
    }
}

function Get-KubectlDryRunParam {
    '--dry-run=client'
}

function Test-ResourceApiVersion([string] $resource, [string] $apiVersion) {

    $Local:ErrorActionPreference = 'SilentlyContinue'
    kubectl explain $resource --api-version $apiVersion *>&1 | out-null
    0 -eq $LASTEXITCODE
}

function Test-CertificateSigningRequestV1Beta1 {

    Test-ResourceApiVersion 'CertificateSigningRequest' 'certificates.k8s.io/v1beta1'
}

function Test-CertificateSigningRequestV1Beta1 {

    Test-ResourceApiVersion 'CustomResourceDefinition' 'apiextensions.k8s.io/v1beta1'
}

function Test-DeploymentLabel([string] $namespace, [string] $labelName, [string] $labelValue) {

    if (-not (Test-Namespace($namespace))) {
        return $false
    }

    $Local:ErrorActionPreference = 'SilentlyContinue'
    $deployments = kubectl -n $namespace get deployment -l "$labelName=$labelValue" -o json | convertfrom-json
    $deployments.items.length -ne 0
}

function Test-SetupKubernetesVersion([ref] $messages, $k8sRequiredMajorVersion, $k8sMinimumMinorVersion, $k8sMaximumMinorVersion) {

    $messages.Value = @()

    if ((Get-KubectlServerVersionMajor) -ne $k8sRequiredMajorVersion) {
        $messages.Value += "Unable to continue because the version of the selected Kubernetes cluster is unsupported (the kubectl server major version is not $k8sRequiredMajorVersion)."
    } else {
        $serverVersionMinor = Get-KubectlServerVersionMinor
        if ($serverVersionMinor -lt $k8sMinimumMinorVersion -or $serverVersionMinor -gt $k8sMaximumMinorVersion) {
            $messages.Value += "Unable to continue because the version of the selected Kubernetes cluster ($serverVersionMinor) is unsupported (the kubectl server minor version must be between $k8sMinimumMinorVersion and $k8sMaximumMinorVersion)."
        } else {
            $clientVersion = Get-KubectlClientVersion
            $serverVersion = Get-KubectlServerVersion
            if ($clientVersion -ne $serverVersion) {
                $messages.Value += "Unable to continue because the kubectl client version ($clientVersion) does not match the Kubernetes cluster version ($serverVersion)."
            }
        }
    }
    return $messages.Value.Length -eq 0
}

function Get-VirtualCpuCountFromReservation([string] $cpuReservation) {

    if ($cpuReservation -notmatch '(?<cpu>(\d+))m?') {
        throw "CPU reservation is not formatted correctly. Specify CPU count with optional 'm' suffix"
    }

    $cpuReservationNoSuffix = [convert]::ToSingle($matches.cpu)
    if ($cpuReservationNoSuffix -gt 64) {
        $cpuReservationNoSuffix = $cpuReservationNoSuffix / 1000
    }

    [math]::Floor($cpuReservationNoSuffix)
}