Skip to content

WIP: Added retry logic honoring graph throttling guardrails #232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: |-

# terraform-provider-microsoft365 Provider

The community Microsoft 365 provider allows managing environments and other resources within [Power Platform](https://powerplatform.microsoft.com/).
The community Microsoft 365 provider allows managing environments and other resources within [Microsoft 365](https://www.microsoft.com/en-gb/microsoft-365/products-apps-services).

!> This code is made available as a public preview. Features are being actively developed and may have restricted or limited functionality. Future updates may introduce breaking changes, but we follow [Semantic Versioning](https://semver.org/) to help mitigate this. The software may contain bugs, errors, or other issues that could cause service interruption or data loss. We recommend backing up your data and testing in non-production environments. Your feedback is valuable to us, so please share any issues or suggestions you encounter via GitHub issues.

Expand Down Expand Up @@ -192,7 +192,7 @@ The provider supports additional configuration options for client behavior, tele
provider "microsoft365" {
# ... authentication configuration ...

debug_mode = false # ENV: M365_DEBUG_MODE
debug_mode = false # ENV: M365_DEBUG_MODE
telemetry_optout = false # ENV: M365_TELEMETRY_OPTOUT

client_options = {
Expand Down Expand Up @@ -270,7 +270,7 @@ variable "cloud" {
variable "tenant_id" {
description = "The M365 tenant ID for the Entra ID application. This ID uniquely identifies your Entra ID (EID) instance. It can be found in the Azure portal under Entra ID > Properties. Can also be set using the `M365_TENANT_ID` environment variable."
type = string
default = "2fd6bb84-1234-abcd-9369-1235b25c1234"
default = ""
}

variable "auth_method" {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
page_title: "microsoft365_graph_beta_device_and_app_management_assignment_filter Resource - terraform-provider-microsoft365"
subcategory: "Intune Assignment Filter"
subcategory: "Intune: Assignment Filter"
description: |-
Manages Assignment Filters in Microsoft Intune.
---
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
page_title: "microsoft365_graph_beta_device_and_app_management_macos_platform_script Resource - terraform-provider-microsoft365"
subcategory: "Intune Device Management Script"
subcategory: "Intune: Device Platform Script"
description: |-
Manages an Intune macOS platform script using the 'deviceShellScripts' Graph Beta API.
---
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
page_title: "microsoft365_graph_beta_device_and_app_management_settings_catalog Resource - terraform-provider-microsoft365"
subcategory: "Intune Settings Catalog"
subcategory: "Intune: Device Configuration"
description: |-
Manages a Settings Catalog policy in Microsoft Intune for Windows, macOS, iOS/iPadOS and Android.
---
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
page_title: "microsoft365_graph_beta_device_and_app_management_windows_platform_script Resource - terraform-provider-microsoft365"
subcategory: "Intune Device Management Script"
subcategory: "Intune: Device Platform Script"
description: |-
Manages an Intune windows platform script using the 'deviceManagementScripts' Graph Beta API.
---
Expand Down
121 changes: 0 additions & 121 deletions internal/client/custom_get_request.go

This file was deleted.

204 changes: 204 additions & 0 deletions internal/client/graphcustom/get_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package graphcustom

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/hashicorp/terraform-plugin-log/tflog"
abstractions "github.com/microsoft/kiota-abstractions-go"
)

// GetRequestConfig contains the configuration for a custom GET request
type GetRequestConfig struct {
// The API version to use (beta or v1.0)
APIVersion GraphAPIVersion
// The base endpoint (e.g., "deviceManagement/configurationPolicies")
Endpoint string
// The endpoint suffix appended after the ID (e.g., "/settings"). Optional.
EndpointSuffix string
// The resource ID syntax format (e.g., "('id')" or "(id)")
ResourceIDPattern string
// The ID of the resource
ResourceID string
// Optional query parameters to include in the request
QueryParameters map[string]string
}

// ODataResponse represents the structure of an OData response
type ODataResponse struct {
// Value is the array of JSON messages returned by the request
Value []json.RawMessage `json:"value"`
// NextLink is the URL for the next page of results used by pagination
NextLink string `json:"@odata.nextLink,omitempty"`
}

// GetRequestByResourceId performs a custom GET request using the Microsoft Graph SDK when the operation
// is not available in the generated SDK methods or when using raw json is easier to handle for response handling.
// This function supports both Beta and V1.0 Graph API versions and automatically handles OData pagination if present.
//
// e.g., GET https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('191056b1-4c4a-4871-8518-162a105d011a')/settings
//
// The function handles:
// - Construction of the Graph API URL with proper formatting
// - Setting up the GET request with optional query parameters
// - Sending the request with proper authentication
// - Automatic pagination if the response is an OData response with a nextLink
// - Combining paginated results into a single response
// - Returning the raw JSON response
//
// Parameters:
// - ctx: The context for the request, which can be used for cancellation and timeout
// - adapter: The request adapter for sending the request
// - config: GetRequestConfig containing:
// - APIVersion: The Graph API version to use (Beta or V1.0)
// - Endpoint: The resource endpoint path (e.g., "/deviceManagement/configurationPolicies")
// - ResourceID: The ID of the resource to retrieve
// - ResourceIDPattern: The format for the resource ID (e.g., "('id')" or "(id)")
// - EndpointSuffix: Optional suffix to append after the resource ID (e.g., "/settings")
// - QueryParameters: Optional query parameters for the request
//
// Returns:
// - json.RawMessage: The raw JSON response from the GET request. For paginated responses,
// returns a combined response with all results in the "value" array
// - error: Returns nil if the request was successful, otherwise an error describing what went wrong
//
// Example Usage:
//
// config := GetRequestConfig{
// APIVersion: GraphAPIBeta,
// Endpoint: "/deviceManagement/configurationPolicies",
// ResourceID: "d557c813-b8e5-4efc-b00e-9c0bd5fd10df",
// ResourceIDPattern: "('id')",
// EndpointSuffix: "/settings",
// QueryParameters: map[string]string{
// "$expand": "children",
// },
// }
//
// response, err := GetRequestByResourceId(ctx, adapter, config)
// if err != nil {
// log.Fatalf("Error: %v", err)
// }
//
// fmt.Printf("Response: %+v\n", response)
func GetRequestByResourceId(ctx context.Context, adapter abstractions.RequestAdapter, reqConfig GetRequestConfig) (json.RawMessage, error) {

requestInfo := abstractions.NewRequestInformation()
requestInfo.Method = abstractions.GET
requestInfo.UrlTemplate = ByIDRequestUrlTemplate(reqConfig)
requestInfo.PathParameters = map[string]string{
"baseurl": fmt.Sprintf("https://graph.microsoft.com/%s", reqConfig.APIVersion),
}
requestInfo.Headers.Add("Accept", "application/json")

if reqConfig.QueryParameters != nil {
for key, value := range reqConfig.QueryParameters {
requestInfo.QueryParameters[key] = value
}
}

// Make initial request
body, err := makeRequest(ctx, adapter, requestInfo)
if err != nil {
return nil, err
}

// Try to parse as OData response to check for pagination
var firstResponse ODataResponse
if err := json.Unmarshal(body, &firstResponse); err != nil {
// Not an OData response, return the raw body
return body, nil
}

// If no NextLink or no Value array, this isn't a paginated response
if firstResponse.NextLink == "" || firstResponse.Value == nil {
return body, nil
}

// Handle pagination
var allResults []json.RawMessage
allResults = append(allResults, firstResponse.Value...)
nextLink := firstResponse.NextLink

tflog.Debug(ctx, "Pagination detected, retrieving additional pages", map[string]interface{}{
"itemsRetrieved": len(allResults),
})

for nextLink != "" {
requestInfo = abstractions.NewRequestInformation()
requestInfo.Method = abstractions.GET
requestInfo.UrlTemplate = nextLink
requestInfo.Headers.Add("Accept", "application/json")

body, err = makeRequest(ctx, adapter, requestInfo)
if err != nil {
return nil, err
}

var pageResponse ODataResponse
if err := json.Unmarshal(body, &pageResponse); err != nil {
return nil, fmt.Errorf("error parsing paginated response: %w", err)
}

allResults = append(allResults, pageResponse.Value...)
nextLink = pageResponse.NextLink

tflog.Debug(ctx, "Retrieved additional page", map[string]interface{}{
"itemsRetrieved": len(allResults),
"hasNextPage": nextLink != "",
})
}

combinedResponse := map[string]interface{}{
"value": allResults,
}

return json.Marshal(combinedResponse)
}

// makeRequest executes an HTTP request using the provided Kiota request adapter and request information.
// This helper function handles the conversion of Kiota's RequestInformation into a native HTTP request,
// executes the request, and returns the raw response body.
//
// Parameters:
// - ctx: The context for the request, which can be used for cancellation and timeout
// - adapter: The Kiota request adapter that converts RequestInformation to a native request
// - requestInfo: The Kiota RequestInformation containing the request configuration
//
// Returns:
// - []byte: The raw response body from the HTTP request
// - error: Returns nil if the request was successful, otherwise an error describing what went wrong
//
// The function performs the following steps:
// 1. Converts the Kiota RequestInformation to a native HTTP request
// 2. Executes the HTTP request using a standard http.Client
// 3. Reads and returns the complete response body
func makeRequest(ctx context.Context, adapter abstractions.RequestAdapter, requestInfo *abstractions.RequestInformation) ([]byte, error) {
nativeReq, err := adapter.ConvertToNativeRequest(ctx, requestInfo)
if err != nil {
return nil, fmt.Errorf("error converting to native HTTP request: %w", err)
}

httpReq := nativeReq.(*http.Request)
client := &http.Client{}

tflog.Debug(ctx, "Making request", map[string]interface{}{
"url": httpReq.URL.String(),
})

resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return body, nil
}
Loading