Public/Invoke-SDPRestCall.ps1

<#
    .SYNOPSIS
    Custom rest call for Kaminario K2 platform

    .DESCRIPTION
    GET filters are sent to the API as a query string built from -parameterList.
    The api wants bare key=value for equality, key__in=a,b for lists, and
    key.ref=/path/id for nested ref fields. All of that is derived from the
    parameter list here so cmdlets just hand over $PSBoundParameters.

    -legacyFilter fetches the whole collection (__limit) and filters client
    side the old way, including the .ref parser. Kept for testing/validation.

    .EXAMPLE
    (after logging into a K2)
    Invoke-SDPRestCall -endpoint volume_groups -method GET
    This will return the .hits return for the https://{k2Server}/api/v2/volume_groups API endpoint using the method GET.

    .EXAMPLE
    Invoke-SDPRestCall -endpoint volume -method PATCH -body $body -context TestDev
    This will render the .hits return for the https://{k2Server}/api/v2/volumes API endpoint.

    .NOTES
    Authored by J.R. Phillips (GitHub: JayAreP)

    .LINK
    https://github.com/silk-us/silk-sdp-powershell-sdk
#>


function Invoke-SDPRestCall {
    param(
        [parameter(Mandatory)]
        [string] $endpoint,
        [parameter(Mandatory)]
        [ValidateSet('GET','POST','PATCH','DELETE')]
        [string] $method,
        [parameter()]
        [array] $body,
        [parameter()]
        [hashtable] $parameterList,
        [parameter()]
        [string] $context = 'sdpconnection',
        [parameter()]
        [int] $limit = 9999,
        [parameter()]
        [switch] $legacyFilter,
        [parameter()]
        [switch] $strictURI,
        [parameter()]
        [switch] $strictString,
        [parameter()]
        [array] $strictURIgte,
        [parameter()]
        [array] $strictURIlte,
        [parameter()]
        [switch] $noLimit,
        [parameter()]
        [switch] $fullResponse,
        [parameter()]
        [int] $timeOut = 15
    )

    # strictURI / strictString are no-ops now, server side filtering is the default.
    # left in so older callers dont blow up.

    $endpointURI = (New-SDPURI -endpoint $endpoint -context $context).TrimEnd('?','&')

    # drop the common params and context so we only walk real filters

    if ($parameterList) {
        foreach ($p in [System.Management.Automation.PSCmdlet]::CommonParameters) {
            $parameterList.Remove($p) | Out-Null
        }
        $parameterList.Remove('context') | Out-Null
    }

    # Build the query string for GET. Goes out via -Body which Invoke-RestMethod
    # turns into a url encoded query string for us.
    #
    # @{ref=/hosts/1} -> host.ref=/hosts/1
    # array -> key__in=a,b,c
    # int (gte/lte) -> key__gt / key__lt
    # everything else -> key=value
    #
    # __limit only gets added when there are no filters, otherwise the api
    # default is fine (filtered sets are small). Pass -limit explicitly to force it.

    $queryParams = @{}

    if ($method -eq 'GET' -and -not $legacyFilter) {
        if ($parameterList -and $parameterList.Count -gt 0) {
            Write-Verbose "-- REST using parameters --"
            $parameterList | ConvertTo-Json -Depth 10 | Write-Verbose

            foreach ($p in $parameterList.Keys) {
                $value = $parameterList[$p]
                if ($null -eq $value) {
                    continue
                }

                if ($value -is [array]) {
                    $queryParams.Add("${p}__in", ($value -join ','))
                } elseif ($value.ref) {
                    $queryParams.Add("$p.ref", $value.ref)
                } elseif ($value -is [int] -and $strictURIgte -contains $p) {
                    $queryParams.Add("${p}__gt", $value)
                } elseif ($value -is [int] -and $strictURIlte -contains $p) {
                    $queryParams.Add("${p}__lt", $value)
                } else {
                    $queryParams.Add($p, $value)
                }
            }
        }

        if (-not $noLimit -and ($queryParams.Count -eq 0 -or $PSBoundParameters.ContainsKey('limit'))) {
            $queryParams.Add('__limit', $limit)
        }

        if ($queryParams.Count -gt 0) {
            Write-Verbose "-- REST query string --"
            $queryParams | ConvertTo-Json -Depth 10 | Write-Verbose
        }
    } elseif ($method -eq 'GET' -and -not $noLimit) {
        # legacy, grab everything and filter below
        $endpointURI = $endpointURI + '?__limit=' + $limit
        if ($parameterList -and $parameterList.Count -gt 0) {
            Write-Verbose "-- REST using parameters (legacy post-fetch filter) --"
            $parameterList | ConvertTo-Json -Depth 10 | Write-Verbose
        }
    }

    # JSON body for POST/PATCH.

    if ($body) {
        $bodyjson = $body | ConvertTo-Json -Depth 10
        Write-Verbose "-- REST Using following JSON body --"
        Write-Verbose $bodyjson
    }

    Write-Verbose "Invoke-SDPRestCall --> Requesting $method from $endpointURI <--- Final URI"

    # declare the requested context's credential information

    $restContext = Get-Variable -Scope Global -Name $context -ValueOnly -ErrorAction SilentlyContinue
    if (-not $restContext) {
        Write-Error "No SDP session found for context '$context'. Run 'Connect-SDP' (or pass -context <name> if you connected with a custom context name)." -Category AuthenticationError
        return
    }

    # Make the call.

    $resolveRestException = {
        param($errorRecord)

        $statusCode = $null
        if ($errorRecord.Exception.Response) {
            try {
                $statusCode = [int]$errorRecord.Exception.Response.StatusCode
            } catch {

            }
        }

        # Case 1: HTTP success, deserializer choked on empty body.
        if ($statusCode -and $statusCode -ge 200 -and $statusCode -lt 300) {
            Write-Verbose "Invoke-RestMethod threw on empty-body HTTP $statusCode; treating as success."
            return $true
        }

        # Case 2: real failure. Build the most informative message we can.
        $detailMsg = $errorRecord.ErrorDetails.Message
        $msg = $null
        if (-not [string]::IsNullOrWhiteSpace($detailMsg)) {
            try {
                $parsed = $detailMsg | ConvertFrom-Json -ErrorAction Stop
                if ($parsed.error_msg) {
                    $msg = $parsed.error_msg
                } else {
                    $msg = $detailMsg
                }
            } catch {
                $msg = $detailMsg
            }
        }
        if (-not $msg) {
            if ($statusCode) {
                $msg = "API request failed with HTTP $statusCode and no response body."
            } else {
                $msg = $errorRecord.Exception.Message
            }
        }
        Write-Error $msg
        return $false
    }

    $restSplat = @{
        Method     = $method
        Uri        = $endpointURI
        Credential = $restContext.credentials
        TimeoutSec = $timeOut
    }
    if ($body) {
        $restSplat.Body        = $bodyjson
        $restSplat.ContentType = 'application/json'
    } elseif ($queryParams.Count -gt 0) {
        $restSplat.Body = $queryParams
    }

    if ($PSVersionTable.PSEdition -eq 'Core') {
        $restSplat.SkipCertificateCheck = $true
    } elseif ($PSVersionTable.PSEdition -eq 'Desktop') {
        if ([System.Net.ServicePointManager]::CertificatePolicy -notlike 'TrustAllCertsPolicy') {
            Write-Verbose "Correcting certificate policy"
            Unblock-CertificatePolicy
        }
        if ([Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') {
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol + 'Tls12'
        }
    }

    try {
        $results = Invoke-RestMethod @restSplat
    } catch {
        if (& $resolveRestException $_) {
            $results = $null
        } else {
            return
        }
    }

    if ($fullResponse) {
        return $results
    } else {
        $results = $results.hits
    }

    # LEgacy post-fetch filter - no longer used. Kept for testing, only invoked if -legacyFilter is specified.

    if ($legacyFilter -and $parameterList.Count -gt 0) {
        $rcount = $results.Count
        Write-Verbose "Found $rcount results"
        if ($parameterList.keys) {
            $searchkeys = $parameterList.keys.split()
        }

        foreach ($i in $searchkeys) {
            Write-Verbose "Working with key: $i"
            $parseTarget = $parameterList[$i]
            if ($parseTarget.ref) {
                $results = $results | where-object {$_.$i.ref -eq $parseTarget.ref}
                $rcount = $results.Count
                Write-Verbose "Searching for key $parseTarget as REF"
                Write-Verbose "Found $rcount results for key $i"
            } else {
                $results = $results | where-object {$_.$i -eq $parseTarget}
                $rcount = $results.Count
                Write-Verbose "Searching for key $parseTarget"
                Write-Verbose "Found $rcount results for key $i"
            }

        }
    }

    # Return the results of the call back to the cmdlet.
    foreach ($o in $results) {
        if ($o.id) {
            $o | Add-Member -MemberType NoteProperty -Name 'pipeId' -Value $o.id
        }
        if ($o.name) {
            $o | Add-Member -MemberType NoteProperty -Name 'pipeName' -Value $o.name
        }
    }
    if ($restContext.throttleCorrection.IsPresent) {
        Start-Sleep -Seconds 1
    }
    # empty POST/DELETE reply shouldnt leak a $null into the callers output
    if ($null -eq $results) {
        return
    }
    return $results
}