Skip to content

Commit

Permalink
Merge pull request #232 from deploymenttheory/feat_intune_settings_te…
Browse files Browse the repository at this point in the history
…mplates

WIP: Added retry logic honoring graph throttling guardrails
  • Loading branch information
ShocOne authored Dec 1, 2024
2 parents baef56e + b207c2d commit 797d783
Show file tree
Hide file tree
Showing 60 changed files with 1,312 additions and 761 deletions.
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

0 comments on commit 797d783

Please sign in to comment.