PowerShellForGitHub/GitHubGraphQl.ps1

366 строки
13 KiB
PowerShell

function Invoke-GHGraphQl
{
<#
.SYNOPSIS
A wrapper around Invoke-WebRequest that understands the GitHub GraphQL API.
.DESCRIPTION
A very heavy wrapper around Invoke-WebRequest that understands the GitHub QraphQL API.
It also understands how to parse and handle errors from the GraphQL calls.
The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub
.PARAMETER Description
A friendly description of the operation being performed for logging.
.PARAMETER Body
This parameter forms the body of the request. It will be automatically
encoded to UTF8 and sent as Content Type: "application/json; charset=UTF-8"
.PARAMETER AccessToken
If provided, this will be used as the AccessToken for authentication with the
GraphQL Api as opposed to requesting a new one.
.PARAMETER TelemetryEventName
If provided, the successful execution of this GraphQL command will be logged to telemetry
using this event name.
.PARAMETER TelemetryProperties
If provided, the successful execution of this GraphQL command will be logged to telemetry
with these additional properties. This will be silently ignored if TelemetryEventName
is not provided as well.
.PARAMETER TelemetryExceptionBucket
If provided, any exception that occurs will be logged to telemetry using this bucket.
It's possible that users will wish to log exceptions but not success (by providing
TelemetryEventName) if this is being executed as part of a larger scenario. If this
isn't provided, but TelemetryEventName *is* provided, then TelemetryEventName will be
used as the exception bucket value in the event of an exception. If neither is specified,
no bucket value will be used.
.OUTPUTS
PSCustomObject
.EXAMPLE
Invoke-GHGraphQl
.NOTES
This wraps Invoke-WebRequest as opposed to Invoke-RestMethod because we want access
to the headers that are returned in the response, and Invoke-RestMethod drops those headers.
#>
[CmdletBinding()]
[OutputType([System.Management.Automation.ErrorRecord])]
param(
[string] $Description,
[Parameter(Mandatory)]
[string] $Body,
[string] $AccessToken,
[string] $TelemetryEventName = $null,
[hashtable] $TelemetryProperties = @{},
[string] $TelemetryExceptionBucket = $null
)
Invoke-UpdateCheck
# Telemetry-related
$stopwatch = New-Object -TypeName System.Diagnostics.Stopwatch
$localTelemetryProperties = @{}
$TelemetryProperties.Keys | ForEach-Object { $localTelemetryProperties[$_] = $TelemetryProperties[$_] }
$errorBucket = $TelemetryExceptionBucket
if ([String]::IsNullOrEmpty($errorBucket))
{
$errorBucket = $TelemetryEventName
}
$stopwatch.Start()
$hostName = $(Get-GitHubConfiguration -Name 'ApiHostName')
if ($hostName -eq 'github.com')
{
$url = "https://api.$hostName/graphql"
}
else
{
$url = "https://$hostName/api/v3/graphql"
}
$headers = @{
'User-Agent' = 'PowerShellForGitHub'
}
$AccessToken = Get-AccessToken -AccessToken $AccessToken
if (-not [String]::IsNullOrEmpty($AccessToken))
{
$headers['Authorization'] = "token $AccessToken"
}
$timeOut = Get-GitHubConfiguration -Name WebRequestTimeoutSec
$method = 'Post'
Write-Log -Message $Description -Level Debug
Write-Log -Message "Accessing [$method] $url [Timeout = $timeOut]" -Level Debug
if (Get-GitHubConfiguration -Name LogRequestBody)
{
Write-Log -Message $Body -Level Debug
}
$bodyAsBytes = [System.Text.Encoding]::UTF8.GetBytes($Body)
# Disable Progress Bar in function scope during Invoke-WebRequest
$ProgressPreference = 'SilentlyContinue'
# Save Current Security Protocol
$originalSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol
# Enforce TLS v1.2 Security Protocol
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$invokeWebRequestParms = @{
Uri = $url
Method = $method
Headers = $headers
Body = $bodyAsBytes
UseDefaultCredentials = $true
UseBasicParsing = $true
TimeoutSec = $timeOut
Verbose = $false
}
try
{
$result = Invoke-WebRequest @invokeWebRequestParms
}
catch
{
$ex = $_.Exception
<#
PowerShell 5 Invoke-WebRequest returns a 'System.Net.WebException' object on error.
PowerShell 6+ Invoke-WebRequest returns a 'Microsoft.PowerShell.Commands.HttpResponseException' or
a 'System.Net.Http.HttpRequestException' object on error.
#>
if ($ex.PSTypeNames[0] -eq 'System.Net.Http.HttpRequestException')
{
Write-Debug -Message "Processing PowerShell Core 'System.Net.Http.HttpRequestException'"
$newErrorRecordParms = @{
ErrorMessage = $ex.Message
ErrorId = $_.FullyQualifiedErrorId
ErrorCategory = $_.CategoryInfo.Category
TargetObject = $_.TargetObject
}
$errorRecord = New-ErrorRecord @newErrorRecordParms
Write-Log -Exception $errorRecord -Level Error
Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties
$PSCmdlet.ThrowTerminatingError($errorRecord)
}
elseif ($ex.PSTypeNames[0] -eq 'Microsoft.PowerShell.Commands.HttpResponseException' -or
$ex.PSTypeNames[0] -eq 'System.Net.WebException')
{
Write-Debug -Message "Processing '$($ex.PSTypeNames[0])'"
$errorMessage = @()
$errorMessage += $ex.Message
$errorDetailsMessage = $_.ErrorDetails.Message
if (-not [string]::IsNullOrEmpty($errorDetailsMessage))
{
Write-Debug -Message "Processing Error Details message '$errorDetailsMessage'"
try
{
Write-Debug -Message 'Checking Error Details message for JSON content'
$errorDetailsMessageJson = $errorDetailsMessage | ConvertFrom-Json
}
catch [System.ArgumentException]
{
# Will be thrown if $errorDetailsMessage isn't JSON content
Write-Debug -Message 'No Error Details Message JSON content Found'
$errorDetailsMessageJson = $false
}
if ($errorDetailsMessageJson)
{
Write-Debug -Message 'Adding Error Details Message JSON content to output'
Write-Debug -Message "Error Details Message: $($errorDetailsMessageJson.message)"
Write-Debug -Message "Error Details Documentation URL: $($errorDetailsMessageJson.documentation_url)"
$errorMessage += ($errorDetailsMessageJson.message.Trim() +
' | ' + $errorDetailsMessageJson.documentation_url.Trim())
if ($errorDetailsMessageJson.details)
{
$errorMessage += $errorDetailsMessageJson.details | Format-Table | Out-String
}
}
else
{
# In this case, it's probably not a normal message from the API
Write-Debug -Message 'Adding Error Details Message String to output'
$errorMessage += $_.ErrorDetails.Message | Out-String
}
}
if (-not [System.String]::IsNullOrEmpty($ex.Response))
{
Write-Debug -Message "Processing '$($ex.Response.PSTypeNames[0])' Object"
<#
PowerShell 5.x returns a 'System.Net.HttpWebResponse' exception response object and
PowerShell 6+ returns a 'System.Net.Http.HttpResponseMessage' exception response object.
#>
$requestId = ''
if ($ex.Response.PSTypeNames[0] -eq 'System.Net.HttpWebResponse')
{
if (($ex.Response.Headers.Count -gt 0) -and
(-not [System.String]::IsNullOrEmpty($ex.Response.Headers['X-GitHub-Request-Id'])))
{
$requestId = $ex.Response.Headers['X-GitHub-Request-Id']
}
}
elseif ($ex.Response.PSTypeNames[0] -eq 'System.Net.Http.HttpResponseMessage')
{
$requestId = ($ex.Response.Headers | Where-Object -Property Key -eq 'X-GitHub-Request-Id').Value
}
if (-not [System.String]::IsNullOrEmpty($requestId))
{
Write-Debug -Message "GitHub RequestID '$requestId' in response header"
$localTelemetryProperties['RequestId'] = $requestId
$requestIdMessage += "RequestId: $requestId"
$errorMessage += $requestIdMessage
Write-Log -Message $requestIdMessage -Level Debug
}
}
$newErrorRecordParms = @{
ErrorMessage = $errorMessage -join [Environment]::NewLine
ErrorId = $_.FullyQualifiedErrorId
ErrorCategory = $_.CategoryInfo.Category
TargetObject = $Body
}
$errorRecord = New-ErrorRecord @newErrorRecordParms
Write-Log -Exception $errorRecord -Level Error
Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties
$PSCmdlet.ThrowTerminatingError($errorRecord)
}
else
{
Write-Debug -Message "Processing Other Exception '$($ex.PSTypeNames[0])'"
$newErrorRecordParms = @{
ErrorMessage = $ex.Message
ErrorId = $_.FullyQualifiedErrorId
ErrorCategory = $_.CategoryInfo.Category
TargetObject = $body
}
$errorRecord = New-ErrorRecord @newErrorRecordParms
Write-Log -Exception $errorRecord -Level Error
Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties
$PSCmdlet.ThrowTerminatingError($errorRecord)
}
}
finally
{
# Restore original security protocol
[Net.ServicePointManager]::SecurityProtocol = $originalSecurityProtocol
}
# Record the telemetry for this event.
$stopwatch.Stop()
if (-not [String]::IsNullOrEmpty($TelemetryEventName))
{
$telemetryMetrics = @{ 'Duration' = $stopwatch.Elapsed.TotalSeconds }
Set-TelemetryEvent -EventName $TelemetryEventName -Properties $localTelemetryProperties -Metrics $telemetryMetrics -Verbose:$false
}
Write-Debug -Message "GraphQl result: '$($result.Content)'"
$graphQlResult = $result.Content | ConvertFrom-Json
if ($graphQlResult.errors)
{
Write-Debug -Message "GraphQl Error: $($graphQLResult.errors | Out-String)"
if (-not [System.String]::IsNullOrEmpty($graphQlResult.errors[0].type))
{
$errorId = $graphQlResult.errors[0].type
switch ($errorId)
{
'NOT_FOUND'
{
Write-Debug -Message "GraphQl Error Type: $errorId"
$errorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound
}
Default
{
Write-Debug -Message "GraphQL Unknown Error Type: $errorId"
$errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation
}
}
}
else
{
Write-Debug -Message "GraphQl Unspecified Error"
$errorId = 'UnspecifiedError'
$errorCategory = [System.Management.Automation.ErrorCategory]::NotSpecified
}
$errorMessage = @()
$errorMessage += "GraphQl Error: $($graphQlResult.errors[0].message)"
if ($result.Headers.Count -gt 0 -and
-not [System.String]::IsNullOrEmpty($result.Headers['X-GitHub-Request-Id']))
{
$requestId = $result.Headers['X-GitHub-Request-Id']
$requestIdMessage += "RequestId: $requestId"
$errorMessage += $requestIdMessage
Write-Log -Message $requestIdMessage -Level Debug
}
$newErrorRecordParms = @{
ErrorMessage = $errorMessage -join [Environment]::NewLine
ErrorId = $errorId
ErrorCategory = $errorCategory
TargetObject = $Body
}
$errorRecord = New-ErrorRecord @newErrorRecordParms
Write-Log -Exception $errrorRecord -Level Error
$PSCmdlet.ThrowTerminatingError($errorRecord)
}
else
{
return $graphQlResult
}
}