Private/Invoke-Rest.ps1

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

<#
    .SYNOPSIS
    Executes an HTTP REST API and provides output with error handling.
 
    .PARAMETER Method
    Required. HTTP Method to invoke. Accepted values: GET, POST, PUT, PATCH, DELETE
 
    .PARAMETER Uri
    Required. URI of target resource or operation. Do not include the Azure Resource Manager domain.
 
    .PARAMETER Body
    Optional. HTTP request body.
 
    .PARAMETER CommandName
    Required. Name of the PowerShell command being run. This is included in backend telemetry.
 
    .PARAMETER ParameterSetName
    Optional. Name of the PowerShell parameter set being used. This is included in backend telemetry.
 
    .EXAMPLE
    Invoke-Rest -Method GET -Uri "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/providers/Microsoft.CostManagement/exports/August2023OneTime?api-version=2023-08-01"
 
    Invoke GET method against a target resource URI.
#>

function Invoke-Rest
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("GET", "POST", "PUT", "PATCH", "DELETE")]
        [string]
        $Method,
        
        [Parameter(Mandatory = $true, Position = 1)]
        [string]
        $Uri,
        
        [Parameter()]
        [PSCustomObject]
        $Body,
        
        [Parameter(Mandatory = $true)]
        [string]
        $CommandName,

        [Parameter()]
        [string]
        $ParameterSetName
    )

    $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    $ErrorActionPreference = $PSCmdlet.GetVariableValue('ErrorActionPreference')

    $ver = 'unknown'
    try { $ver = Get-VersionNumber } catch {}

    $arm = (Get-AzContext).Environment.ResourceManagerUrl
    $params = @{
        Method      = $Method
        Uri         = $arm.Trim('/') + '/' + $Uri.Trim('/')
        Headers     = @{
            Authorization             = "Bearer $((Get-AzAccessToken -AsSecureString).Token | ConvertFrom-SecureString -AsPlainText)"
            ClientType                = "FinOpsToolkit.PowerShell.$CommandName@$ver"
            "Content-Type"            = 'application/json'
            "x-ms-command-name"       = "FinOpsToolkit.PowerShell.$CommandName@$ver"
            "x-ms-parameter-set-name" = $ParameterSetName
        }
        ErrorAction = "Stop"
    }
    if ($Body)
    {
        $params.Body = $Body | ConvertTo-Json -Depth 100
    }
    
    Write-Verbose "Invoking $Method $fullUri with request body $Body`n"

    try
    {
        $response = Invoke-WebRequest @params
        $content = $response.Content | ConvertFrom-Json -Depth 100
    }
    catch
    {
        $response = $_.Exception.Response
        try
        {
            $content = $_.ErrorDetails.Message | ConvertFrom-Json -Depth 10
        }
        catch {}

        if ($content.error)
        {
            $errorCode = $content.error.code
            $errorMessage = $content.error.message
            Write-Error -Message $($script:localizedData.Common_ErrorResponse -f $errorMessage, $errorCode)
        }
        else
        {
            throw $_.Exception.Message
        }
    }
    return @{
        Headers    = $response.Headers
        StatusCode = $response.StatusCode
        Success    = $response.StatusCode -ge 200 -and $response.StatusCode -lt 300
        Failure    = $response.StatusCode -ge 300
        NotFound   = $response.StatusCode -eq 404 -or $response.StatusCode -eq 'NotFound'
        Throttled  = $response.StatusCode -eq 429 -or $response.StatusCode -eq 'ResourceRequestsThrottled'
        Content    = $content
    }
}