public/Invoke-ZtAzureRequest.ps1
|
<#
.SYNOPSIS Proxy function for Invoke-AzRestMethod that adds caching, automatic pagination, and OData convenience parameters. .DESCRIPTION This is a proxy function wrapping Invoke-AzRestMethod. It inherits all of Invoke-AzRestMethod's parameter sets (ByPath, ByURI, ByParameters) so it works identically across all Azure environments (Global, USGov, China) without hardcoding any hostnames. Additional features over Invoke-AzRestMethod: * OData query parameters: -Select, -Filter, -Top * Additional query parameters via -QueryParameters hashtable * Session-scoped caching of GET results * Automatic pagination enabled by default for GET requests * Throws on non-2xx errors (consistent with Invoke-ZtGraphRequest) * Return full PSHttpResponse with -FullResponse for StatusCode inspection * Automatic unwrapping of .value array from responses .EXAMPLE Invoke-ZtAzureRequest -Path "/subscriptions?api-version=2022-01-01" Get all subscriptions (api-version in the path, like Invoke-AzRestMethod). .EXAMPLE Invoke-ZtAzureRequest -Path "/subscriptions/$subscriptionId/providers/Microsoft.Compute/virtualMachines?api-version=2024-03-01" -Select "name,location" Get all virtual machines with selected properties. .EXAMPLE $response = Invoke-ZtAzureRequest -Path "/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01" -Filter "atScope()" -FullResponse if ($response.StatusCode -eq 403) { # Handle forbidden access } Get role assignments at root scope with full response to check status code. .EXAMPLE $body = @{ query = "Resources | where type =~ 'microsoft.compute/virtualmachines'" } | ConvertTo-Json Invoke-ZtAzureRequest -Path "/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01" -Method POST -Payload $body Query Azure Resource Graph using POST with pagination (follows $skipToken automatically). .EXAMPLE Invoke-ZtAzureRequest -Uri "https://management.usgovcloudapi.net/subscriptions?api-version=2022-01-01" Use -Uri for full URL (sovereign cloud). All Invoke-AzRestMethod parameters are supported. #> function Invoke-ZtAzureRequest { [CmdletBinding(DefaultParameterSetName = 'ByPath')] param( #region Invoke-AzRestMethod: ByPath parameter set [Parameter(ParameterSetName = 'ByPath', Mandatory, HelpMessage = 'Path of target resource URL. Hostname of Resource Manager should not be added.')] [ValidateNotNullOrEmpty()] [string] ${Path}, #endregion #region Invoke-AzRestMethod: ByURI parameter set [Parameter(ParameterSetName = 'ByURI', Mandatory, Position = 1, HelpMessage = 'Uniform Resource Identifier of the Azure resources.')] [ValidateNotNullOrEmpty()] [uri] ${Uri}, [Parameter(ParameterSetName = 'ByURI', HelpMessage = 'Identifier URI specified by the REST API you are calling.')] [ValidateNotNullOrEmpty()] [uri] ${ResourceId}, #endregion #region Invoke-AzRestMethod: ByParameters parameter set [Parameter(ParameterSetName = 'ByParameters', HelpMessage = 'Target Subscription Id')] [ValidateNotNullOrEmpty()] [string] ${SubscriptionId}, [Parameter(ParameterSetName = 'ByParameters', HelpMessage = 'Target Resource Group Name')] [ValidateNotNullOrEmpty()] [string] ${ResourceGroupName}, [Parameter(ParameterSetName = 'ByParameters', HelpMessage = 'Target Resource Provider Name')] [ValidateNotNullOrEmpty()] [string] ${ResourceProviderName}, [Parameter(ParameterSetName = 'ByParameters', HelpMessage = 'List of Target Resource Type')] [ValidateNotNullOrEmpty()] [string[]] ${ResourceType}, [Parameter(ParameterSetName = 'ByParameters', HelpMessage = 'list of Target Resource Name')] [ValidateNotNullOrEmpty()] [string[]] ${Name}, [Parameter(ParameterSetName = 'ByParameters', Mandatory, HelpMessage = 'Api Version')] [ValidateNotNullOrEmpty()] [string] ${ApiVersion}, #endregion #region Invoke-AzRestMethod: Common parameters [Parameter(HelpMessage = 'Specifies the method used for the web request. Defaults to GET.')] [ValidateSet('GET', 'POST')] [ValidateNotNullOrEmpty()] [string] ${Method}, [Parameter(HelpMessage = 'JSON format payload')] [ValidateNotNullOrEmpty()] [string] ${Payload}, [Parameter(HelpMessage = 'Run cmdlet in the background')] [switch] ${AsJob}, [Parameter(HelpMessage = 'The credentials, account, tenant, and subscription used for communication with Azure.')] [Alias('AzContext', 'AzureRmContext', 'AzureCredential')] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core.IAzureContextContainer] ${DefaultProfile}, #endregion #region ZtAzureRequest: OData convenience parameters # Filters properties (columns). Adds $select query parameter. [Parameter()] [string[]] $Select, # Filters results (rows). Adds $filter query parameter. [Parameter()] [string] $Filter, # The number of items to be included in the result. Adds $top query parameter. [Parameter()] [ValidateRange(1, [int]::MaxValue)] [int] $Top, # Additional query parameters to append to the request URL. [Parameter()] [hashtable] $QueryParameters, #endregion #region ZtAzureRequest: Pagination and caching control # Only return first page of results. By default, -Paginate is enabled. [Parameter()] [switch] $DisablePaging, # Specify if this request should skip cache and go directly to Azure. [Parameter()] [switch] $DisableCache, # Return the full PSHttpResponse object instead of just the parsed content. # When specified, does not throw on non-2xx status codes. [Parameter()] [switch] $FullResponse #endregion ) process { # Determine effective method (default to GET) $effectiveMethod = if ($Method) { $Method } else { 'GET' } #region Build Invoke-AzRestMethod parameters # Start with all bound parameters, then remove our custom ones $azParams = @{} + $PSBoundParameters # Remove ZtAzureRequest-specific parameters (not recognized by Invoke-AzRestMethod) foreach ($customParam in @('Select', 'Filter', 'Top', 'QueryParameters', 'DisablePaging', 'DisableCache', 'FullResponse')) { $azParams.Remove($customParam) | Out-Null } # Inject OData and custom query parameters into -Path or -Uri $extraQueryParams = [ordered]@{} if ($Select) { $extraQueryParams['$select'] = $Select -join ',' } if ($Filter) { $extraQueryParams['$filter'] = $Filter } if ($Top) { $extraQueryParams['$top'] = $Top } if ($QueryParameters) { foreach ($key in $QueryParameters.Keys) { $extraQueryParams[$key] = $QueryParameters[$key] } } if ($extraQueryParams.Count -gt 0) { $extraQs = ConvertTo-QueryString $extraQueryParams if ($azParams.ContainsKey('Path')) { # -Path is a relative path (e.g., "/subscriptions?api-version=...") $pathStr = $azParams['Path'] $fragment = '' if ($pathStr -match '(#.+)$') { $fragment = $matches[1] $pathStr = $pathStr.Substring(0, $pathStr.Length - $fragment.Length) } if ($pathStr -match '\?') { $azParams['Path'] = $pathStr + '&' + $extraQs + $fragment } else { $azParams['Path'] = $pathStr + '?' + $extraQs + $fragment } } elseif ($azParams.ContainsKey('Uri')) { # -Uri is a full URL # Use UriBuilder to safely handle query parameters and fragments $uriBuilder = [System.UriBuilder]::new($azParams['Uri']) if ([string]::IsNullOrWhiteSpace($uriBuilder.Query)) { $uriBuilder.Query = $extraQs } else { # UriBuilder.Query includes the leading '?' $uriBuilder.Query = $uriBuilder.Query.TrimStart('?') + '&' + $extraQs } $azParams['Uri'] = $uriBuilder.Uri } else { # ByParameters set: Invoke-AzRestMethod builds the URL internally, # so we cannot append OData/custom query parameters. Write-PSFMessage -Level Warning -Message "OData/query parameters (Select, Filter, Top, QueryParameters) are not supported with the ByParameters parameter set and will be ignored. Use -Path or -Uri instead." } } # Enable automatic pagination for GET requests only. # POST-based APIs (e.g. Resource Graph) handle paging via request body (skipToken in options). $isGet = $effectiveMethod -eq 'GET' if ($isGet -and -not $DisablePaging -and -not $AsJob) { $azParams['Paginate'] = $true } #endregion Build Invoke-AzRestMethod parameters #region Determine cache key if ($azParams.ContainsKey('Path')) { $cacheKey = $azParams['Path'] } elseif ($azParams.ContainsKey('Uri')) { $cacheKey = $azParams['Uri'].ToString() } else { # ByParameters set: build a key from the components $cacheKey = '{0}/{1}/{2}/{3}/{4}' -f $SubscriptionId, $ResourceGroupName, $ResourceProviderName, ($ResourceType -join '/'), ($Name -join '/') } # Prefix with HTTP method for non-GET requests to prevent cache-key collisions # (e.g. a POST to the same URL as a previous GET should not share a cache entry). if ($effectiveMethod -ne 'GET') { $cacheKey = "$effectiveMethod`:$cacheKey" } #endregion Determine cache key #region Execute with caching $results = Invoke-ZtAzureRequestCache -CacheKey $cacheKey -AzParams $azParams ` -DisableCache:$DisableCache -FullResponse:$FullResponse #endregion Execute with caching #region Format Results if ($FullResponse) { return $results } # Unwrap .value array if present (standard ARM list response envelope) if ($results -and ($results.PSObject.Properties.Name -contains 'value')) { return $results.value } $results #endregion Format Results } } <# .ForwardHelpTargetName Az.Accounts\Invoke-AzRestMethod .ForwardHelpCategory Cmdlet #> |