From 7c6707c189fd5d6c10b904870bca255e0977175e Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 10:13:01 +0000 Subject: [PATCH 01/13] Refactor settings catalog CRUD operations to utilize dynamic endpoint paths; remove commented-out code for improved readability and maintainability. Update custom PUT request configuration to ensure correct API versioning. --- internal/client/custom_put_request.go | 2 +- .../provider/client_credential_factory.go | 14 +++--- .../beta/settings_catalog/crud.go | 47 ++----------------- 3 files changed, 12 insertions(+), 51 deletions(-) diff --git a/internal/client/custom_put_request.go b/internal/client/custom_put_request.go index 1f9fcc57..9dadc8b9 100644 --- a/internal/client/custom_put_request.go +++ b/internal/client/custom_put_request.go @@ -67,7 +67,7 @@ func SendCustomPutRequestByResourceId(ctx context.Context, adapter abstractions. requestInfo.Method = abstractions.PUT requestInfo.UrlTemplate = "{+baseurl}/" + config.Endpoint + "('{id}')" requestInfo.PathParameters = map[string]string{ - "baseurl": fmt.Sprintf("https://graph.microsoft.com/%s", config.APIVersion), + "baseurl": fmt.Sprintf("https://graph.microsoft.com%s", config.APIVersion), "id": config.ResourceID, } diff --git a/internal/provider/client_credential_factory.go b/internal/provider/client_credential_factory.go index 5004dd12..415e7f3d 100644 --- a/internal/provider/client_credential_factory.go +++ b/internal/provider/client_credential_factory.go @@ -38,7 +38,9 @@ func CredentialFactory(authMethod string) (CredentialStrategy, error) { } } -// obtainCredential is now a wrapper that uses the CredentialFactory and CredentialStrategy +// obtainCredential performs the necessary steps to obtain a TokenCredential based on the provider configuration. +// It uses the CredentialFactory and CredentialStrategy to create the appropriate credential type based on the authentication method +// defined within the provider configuraton. func obtainCredential(ctx context.Context, config *M365ProviderModel, clientOptions policy.ClientOptions) (azcore.TokenCredential, error) { tflog.Info(ctx, "Obtaining credential", map[string]interface{}{ "auth_method": config.AuthMethod.ValueString(), @@ -66,7 +68,7 @@ type CredentialStrategy interface { GetCredential(ctx context.Context, config *M365ProviderModel, clientOptions policy.ClientOptions) (azcore.TokenCredential, error) } -// ClientSecretStrategy implements CredentialStrategy for client secret authentication +// ClientSecretStrategy implements the credential strategy for client secret authentication type ClientSecretStrategy struct{} func (s *ClientSecretStrategy) GetCredential(ctx context.Context, config *M365ProviderModel, clientOptions policy.ClientOptions) (azcore.TokenCredential, error) { @@ -87,7 +89,7 @@ func (s *ClientSecretStrategy) GetCredential(ctx context.Context, config *M365Pr }) } -// ClientCertificateStrategy implements CredentialStrategy for client certificate authentication +// ClientCertificateStrategy implements the credential strategy for client certificate authentication type ClientCertificateStrategy struct{} func (s *ClientCertificateStrategy) GetCredential(ctx context.Context, config *M365ProviderModel, clientOptions policy.ClientOptions) (azcore.TokenCredential, error) { @@ -129,7 +131,7 @@ func (s *ClientCertificateStrategy) GetCredential(ctx context.Context, config *M }) } -// UsernamePasswordStrategy implements CredentialStrategy for username/password authentication +// UsernamePasswordStrategy implements the credential strategy for username/password authentication type UsernamePasswordStrategy struct{} func (s *UsernamePasswordStrategy) GetCredential(ctx context.Context, config *M365ProviderModel, clientOptions policy.ClientOptions) (azcore.TokenCredential, error) { @@ -152,7 +154,7 @@ func (s *UsernamePasswordStrategy) GetCredential(ctx context.Context, config *M3 }) } -// DeviceCodeStrategy implements CredentialStrategy for device code authentication +// DeviceCodeStrategy implements the credential strategy for device code authentication type DeviceCodeStrategy struct{} func (s *DeviceCodeStrategy) GetCredential(ctx context.Context, config *M365ProviderModel, clientOptions policy.ClientOptions) (azcore.TokenCredential, error) { @@ -178,7 +180,7 @@ func (s *DeviceCodeStrategy) GetCredential(ctx context.Context, config *M365Prov }) } -// InteractiveBrowserStrategy implements CredentialStrategy for interactive browser authentication +// InteractiveBrowserStrategy implements the credential strategy for interactive browser authentication type InteractiveBrowserStrategy struct{} func (s *InteractiveBrowserStrategy) GetCredential(ctx context.Context, config *M365ProviderModel, clientOptions policy.ClientOptions) (azcore.TokenCredential, error) { diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go index b3682acd..40d5f82a 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go @@ -109,28 +109,9 @@ func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.Creat } MapRemoteResourceStateToTerraform(ctx, &object, respResource) - // respSettings, err := r.client. - // DeviceManagement(). - // ConfigurationPolicies(). - // ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - // Settings(). - // Get(context.Background(), &msgraphsdk.ConfigurationPoliciesItemSettingsRequestBuilderGetRequestConfiguration{ - // QueryParameters: &msgraphsdk.ConfigurationPoliciesItemSettingsRequestBuilderGetQueryParameters{ - // Expand: []string{""}, - // }, - // }) - - // if err != nil { - // errors.HandleGraphError(ctx, err, resp, "Create - Settings Fetch", r.ReadPermissions) - // return - // } - - // settingsList := respSettings.GetValue() - // MapRemoteSettingsStateToTerraform(ctx, &object, settingsList) - settingsConfig := client.CustomGetRequestConfig{ APIVersion: client.GraphAPIBeta, - Endpoint: "deviceManagement/configurationPolicies", + Endpoint: r.ResourcePath, EndpointSuffix: "/settings", ResourceIDPattern: "('id')", ResourceID: object.ID.ValueString(), @@ -217,31 +198,9 @@ func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadReq MapRemoteResourceStateToTerraform(ctx, &object, respResource) - // // Retrieve settings from the response - // respSettings, err := r.client. - // DeviceManagement(). - // ConfigurationPolicies(). - // ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - // Settings(). - // Get(context.Background(), &msgraphsdk.ConfigurationPoliciesItemSettingsRequestBuilderGetRequestConfiguration{ - // QueryParameters: &msgraphsdk.ConfigurationPoliciesItemSettingsRequestBuilderGetQueryParameters{ - // Expand: []string{""}, // Expand all related settings - // }, - // }) - - // if err != nil { - // errors.HandleGraphError(ctx, err, resp, "Read", r.ReadPermissions) - // return - // } - - // // Extract the list of settings from the collection response - // settingsList := respSettings.GetValue() - - // MapRemoteSettingsStateToTerraform(ctx, &object, settingsList) - settingsConfig := client.CustomGetRequestConfig{ APIVersion: client.GraphAPIBeta, - Endpoint: "deviceManagement/configurationPolicies", + Endpoint: r.ResourcePath, EndpointSuffix: "/settings", ResourceIDPattern: "('id')", ResourceID: object.ID.ValueString(), @@ -324,7 +283,7 @@ func (r *SettingsCatalogResource) Update(ctx context.Context, req resource.Updat putRequest := client.CustomPutRequestConfig{ APIVersion: client.GraphAPIBeta, - Endpoint: "deviceManagement/configurationPolicies", + Endpoint: r.ResourcePath, ResourceID: object.ID.ValueString(), RequestBody: requestBody, } From 04140fdf69f0d5a55dbf1e702fcf3c02857d8140 Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 11:28:06 +0000 Subject: [PATCH 02/13] Refactor custom request configurations to standardize endpoint paths; implement PreserveSecretSettings function for recursive handling of secret settings in JSON structures. Enhance error logging for better debugging during normalization processes. --- internal/client/custom_get_request.go | 160 +++++++++++++++--- internal/client/custom_post_request.go | 2 +- internal/client/custom_put_request.go | 6 +- .../common/normalize/settings_catalog.go | 56 ++++++ .../beta/settings_catalog/state_settings.go | 50 +----- 5 files changed, 200 insertions(+), 74 deletions(-) create mode 100644 internal/resources/common/normalize/settings_catalog.go diff --git a/internal/client/custom_get_request.go b/internal/client/custom_get_request.go index 6d40de32..e724ada5 100644 --- a/internal/client/custom_get_request.go +++ b/internal/client/custom_get_request.go @@ -28,9 +28,17 @@ type CustomGetRequestConfig struct { 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"` +} + // SendCustomGetRequestByResourceId 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 during stating operations. -// This function supports both Beta and V1.0 Graph API versions. +// 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 // @@ -38,6 +46,8 @@ type CustomGetRequestConfig struct { // - 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: @@ -45,44 +55,41 @@ type CustomGetRequestConfig struct { // - adapter: The request adapter for sending the request // - config: CustomGetRequestConfig containing: // - APIVersion: The Graph API version to use (Beta or V1.0) -// - Endpoint: The resource endpoint path (e.g., "deviceManagement/configurationPolicies") +// - 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 +// - 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 := CustomGetRequestConfig{ -// APIVersion: GraphAPIBeta, -// Endpoint: "deviceManagement/configurationPolicies('{id}')/settings", -// ResourceID: "d557c813-b8e5-4efc-b00e-9c0bd5fd10df", +// config := CustomGetRequestConfig{ +// APIVersion: GraphAPIBeta, +// Endpoint: "/deviceManagement/configurationPolicies", +// ResourceID: "d557c813-b8e5-4efc-b00e-9c0bd5fd10df", +// ResourceIDPattern: "('id')", +// EndpointSuffix: "/settings", // QueryParameters: map[string]string{ -// "$expand": "children", +// "$expand": "children", // }, // } // -// response, err := SendCustomGetRequestByResourceId(ctx, adapter, config, factory, errorMappings) -// +// response, err := SendCustomGetRequestByResourceId(ctx, adapter, config) // if err != nil { // log.Fatalf("Error: %v", err) // } // -// fmt.Printf("Response: %+v\n", response) +// fmt.Printf("Response: %+v\n", response) func SendCustomGetRequestByResourceId(ctx context.Context, adapter abstractions.RequestAdapter, reqConfig CustomGetRequestConfig) (json.RawMessage, error) { + requestInfo := abstractions.NewRequestInformation() requestInfo.Method = abstractions.GET - - // Build endpoint with ID syntax - idFormat := strings.ReplaceAll(reqConfig.ResourceIDPattern, "id", reqConfig.ResourceID) - endpoint := reqConfig.Endpoint + idFormat - if reqConfig.EndpointSuffix != "" { - endpoint += reqConfig.EndpointSuffix - } - - requestInfo.UrlTemplate = "{+baseurl}/" + endpoint + requestInfo.UrlTemplate = ByIDRequestUrlTemplate(reqConfig) requestInfo.PathParameters = map[string]string{ "baseurl": fmt.Sprintf("https://graph.microsoft.com/%s", reqConfig.APIVersion), } @@ -90,10 +97,111 @@ func SendCustomGetRequestByResourceId(ctx context.Context, adapter abstractions. if reqConfig.QueryParameters != nil { for key, value := range reqConfig.QueryParameters { - requestInfo.QueryParametersAny[key] = value + 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) +} + +// ByIDRequestUrlTemplate constructs a URL template for a single resource request using the provided configuration. +// The function combines the endpoint path with a resource ID and optional suffix to create a complete URL template. +// For example, if the config contains: +// - Endpoint: "/deviceManagement/configurationPolicies" +// - ResourceIDPattern: "('id')" +// - ResourceID: "12345" +// - EndpointSuffix: "/settings" +// +// The resulting template would be: "{+baseurl}/deviceManagement/configurationPolicies('12345')/settings" +// +// Parameters: +// - reqConfig: CustomGetRequestConfig containing the endpoint path, resource ID pattern, actual ID, and optional suffix +// +// Returns: +// - string: The constructed URL template ready for use with the Kiota request adapter +func ByIDRequestUrlTemplate(reqConfig CustomGetRequestConfig) string { + idFormat := strings.ReplaceAll(reqConfig.ResourceIDPattern, "id", reqConfig.ResourceID) + endpoint := reqConfig.Endpoint + idFormat + if reqConfig.EndpointSuffix != "" { + endpoint += reqConfig.EndpointSuffix + } + return "{+baseurl}" + endpoint +} + +// 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 request: %w", err) @@ -102,16 +210,16 @@ func SendCustomGetRequestByResourceId(ctx context.Context, adapter abstractions. 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, fmt.Errorf("error executing request: %w", err) } defer resp.Body.Close() - tflog.Debug(ctx, "Request URL", map[string]interface{}{ - "url": httpReq.URL.String(), - }) - body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response: %w", err) diff --git a/internal/client/custom_post_request.go b/internal/client/custom_post_request.go index ffdecf6b..f490b1a4 100644 --- a/internal/client/custom_post_request.go +++ b/internal/client/custom_post_request.go @@ -12,7 +12,7 @@ import ( type CustomPostRequestConfig struct { // The API version to use (beta or v1.0) APIVersion GraphAPIVersion - // The base endpoint (e.g., "deviceManagement/configurationPolicies") + // The base endpoint (e.g., "/deviceManagement/configurationPolicies") Endpoint string // The request body RequestBody s.Parsable diff --git a/internal/client/custom_put_request.go b/internal/client/custom_put_request.go index 9dadc8b9..23d7719d 100644 --- a/internal/client/custom_put_request.go +++ b/internal/client/custom_put_request.go @@ -20,7 +20,7 @@ const ( type CustomPutRequestConfig struct { // The API version to use (beta or v1.0) APIVersion GraphAPIVersion - // The base endpoint (e.g., "deviceManagement/configurationPolicies") + // The base endpoint (e.g., "/deviceManagement/configurationPolicies") Endpoint string // The ID of the resource ResourceID string @@ -65,9 +65,9 @@ type CustomPutRequestConfig struct { func SendCustomPutRequestByResourceId(ctx context.Context, adapter abstractions.RequestAdapter, config CustomPutRequestConfig) error { requestInfo := abstractions.NewRequestInformation() requestInfo.Method = abstractions.PUT - requestInfo.UrlTemplate = "{+baseurl}/" + config.Endpoint + "('{id}')" + requestInfo.UrlTemplate = "{+baseurl}" + config.Endpoint + "('{id}')" requestInfo.PathParameters = map[string]string{ - "baseurl": fmt.Sprintf("https://graph.microsoft.com%s", config.APIVersion), + "baseurl": fmt.Sprintf("https://graph.microsoft.com/%s", config.APIVersion), "id": config.ResourceID, } diff --git a/internal/resources/common/normalize/settings_catalog.go b/internal/resources/common/normalize/settings_catalog.go new file mode 100644 index 00000000..434becdf --- /dev/null +++ b/internal/resources/common/normalize/settings_catalog.go @@ -0,0 +1,56 @@ +package normalize + +import ( + "fmt" + "reflect" +) + +// PreserveSecretSettings recursively searches through settings catalog HCL JSON structure for secret settings +// and preserves the value and valueState from the config settings. This is performed recursively throughout the JSON +// settings catalog and It returns an error if any unexpected data types or mismatches are encountered. +func PreserveSecretSettings(config, resp interface{}) error { + switch configV := config.(type) { + case map[string]interface{}: + respV, ok := resp.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected map[string]interface{} in response, got %s", reflect.TypeOf(resp)) + } + + if odataType, ok := configV["@odata.type"].(string); ok && + odataType == "#microsoft.graph.deviceManagementConfigurationSecretSettingValue" { + if value, ok := configV["value"]; ok { + respV["value"] = value + } + if valueState, ok := configV["valueState"]; ok { + respV["valueState"] = valueState + } + return nil + } + + for k, v := range configV { + if respChild, ok := respV[k]; ok { + if err := PreserveSecretSettings(v, respChild); err != nil { + return fmt.Errorf("error in key %q: %w", k, err) + } + } + } + + case []interface{}: + respV, ok := resp.([]interface{}) + if !ok { + return fmt.Errorf("expected []interface{} in response, got %s", reflect.TypeOf(resp)) + } + for i := range configV { + if i < len(respV) { + if err := PreserveSecretSettings(configV[i], respV[i]); err != nil { + return fmt.Errorf("error in array index %d: %w", i, err) + } + } + } + + default: + return fmt.Errorf("unsupported type: %s", reflect.TypeOf(config)) + } + + return nil +} diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/state_settings.go b/internal/resources/device_and_app_management/beta/settings_catalog/state_settings.go index 885d5259..44a88eeb 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/state_settings.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/state_settings.go @@ -47,17 +47,20 @@ func MapRemoteSettingsStateToTerraform(ctx context.Context, data *SettingsCatalo "settingsDetails": settingsContent, } - preserveSecretSettings(configSettings, structuredContent) + if err := normalize.PreserveSecretSettings(configSettings, structuredContent); err != nil { + tflog.Error(ctx, "Error stating settings catalog secret settings from HCL", map[string]interface{}{"error": err.Error()}) + return + } jsonBytes, err := json.Marshal(structuredContent) if err != nil { - tflog.Error(ctx, "Failed to marshal structured content", map[string]interface{}{"error": err.Error()}) + tflog.Error(ctx, "Failed to marshal JSON structured content during preparation for normalization", map[string]interface{}{"error": err.Error()}) return } normalizedJSON, err := normalize.JSONAlphabetically(string(jsonBytes)) if err != nil { - tflog.Error(ctx, "Failed to normalize JSON alphabetically", map[string]interface{}{"error": err.Error()}) + tflog.Error(ctx, "Failed to normalize settings catalog JSON alphabetically", map[string]interface{}{"error": err.Error()}) return } @@ -66,44 +69,3 @@ func MapRemoteSettingsStateToTerraform(ctx context.Context, data *SettingsCatalo data.Settings = types.StringValue(normalizedJSON) } - -// preserveSecretSettings recursively searches through settings catalog HCL JSON structure for secret settings -// and preserves the value and valueState from the config settings. This is used to ensure that secret values -// within the state match the original config settings and do not cause unnecessary updates. -func preserveSecretSettings(config, resp interface{}) { - switch configV := config.(type) { - case map[string]interface{}: - respV, ok := resp.(map[string]interface{}) - if !ok { - return - } - - if odataType, ok := configV["@odata.type"].(string); ok && - odataType == "#microsoft.graph.deviceManagementConfigurationSecretSettingValue" { - if value, ok := configV["value"]; ok { - respV["value"] = value - } - if valueState, ok := configV["valueState"]; ok { - respV["valueState"] = valueState - } - return - } - - for k, v := range configV { - if respChild, ok := respV[k]; ok { - preserveSecretSettings(v, respChild) - } - } - - case []interface{}: - respV, ok := resp.([]interface{}) - if !ok { - return - } - for i := range configV { - if i < len(respV) { - preserveSecretSettings(configV[i], respV[i]) - } - } - } -} From 99a8669c72e7b7a600b09c6268571251ca91c49e Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 11:36:28 +0000 Subject: [PATCH 03/13] Refactor custom request handling to introduce graphcustom package; implement ByIDRequestUrlTemplate and PutRequestByResourceId functions for improved URL construction and PUT request management. Update existing CRUD operations to utilize new configurations, enhancing code organization and maintainability. --- .../get_request.go} | 41 ++++--------------- .../post_request.go} | 20 ++++----- .../put_request.go} | 12 +++--- internal/client/graphcustom/url_templates.go | 27 ++++++++++++ .../endpoint_privilege_management/crud.go | 8 ++-- .../beta/settings_catalog/crud.go | 20 ++++----- 6 files changed, 65 insertions(+), 63 deletions(-) rename internal/client/{custom_get_request.go => graphcustom/get_request.go} (80%) rename internal/client/{custom_post_request.go => graphcustom/post_request.go} (83%) rename internal/client/{custom_put_request.go => graphcustom/put_request.go} (85%) create mode 100644 internal/client/graphcustom/url_templates.go diff --git a/internal/client/custom_get_request.go b/internal/client/graphcustom/get_request.go similarity index 80% rename from internal/client/custom_get_request.go rename to internal/client/graphcustom/get_request.go index e724ada5..a7cb4799 100644 --- a/internal/client/custom_get_request.go +++ b/internal/client/graphcustom/get_request.go @@ -1,4 +1,4 @@ -package client +package graphcustom import ( "context" @@ -6,14 +6,13 @@ import ( "fmt" "io" "net/http" - "strings" "github.com/hashicorp/terraform-plugin-log/tflog" abstractions "github.com/microsoft/kiota-abstractions-go" ) -// CustomGetRequestConfig contains the configuration for a custom GET request -type CustomGetRequestConfig struct { +// 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") @@ -36,7 +35,7 @@ type ODataResponse struct { NextLink string `json:"@odata.nextLink,omitempty"` } -// SendCustomGetRequestByResourceId performs a custom GET request using the Microsoft Graph SDK when the operation +// 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. // @@ -53,7 +52,7 @@ type ODataResponse struct { // Parameters: // - ctx: The context for the request, which can be used for cancellation and timeout // - adapter: The request adapter for sending the request -// - config: CustomGetRequestConfig containing: +// - 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 @@ -68,7 +67,7 @@ type ODataResponse struct { // // Example Usage: // -// config := CustomGetRequestConfig{ +// config := GetRequestConfig{ // APIVersion: GraphAPIBeta, // Endpoint: "/deviceManagement/configurationPolicies", // ResourceID: "d557c813-b8e5-4efc-b00e-9c0bd5fd10df", @@ -79,13 +78,13 @@ type ODataResponse struct { // }, // } // -// response, err := SendCustomGetRequestByResourceId(ctx, adapter, config) +// response, err := GetRequestByResourceId(ctx, adapter, config) // if err != nil { // log.Fatalf("Error: %v", err) // } // // fmt.Printf("Response: %+v\n", response) -func SendCustomGetRequestByResourceId(ctx context.Context, adapter abstractions.RequestAdapter, reqConfig CustomGetRequestConfig) (json.RawMessage, error) { +func GetRequestByResourceId(ctx context.Context, adapter abstractions.RequestAdapter, reqConfig GetRequestConfig) (json.RawMessage, error) { requestInfo := abstractions.NewRequestInformation() requestInfo.Method = abstractions.GET @@ -160,30 +159,6 @@ func SendCustomGetRequestByResourceId(ctx context.Context, adapter abstractions. return json.Marshal(combinedResponse) } -// ByIDRequestUrlTemplate constructs a URL template for a single resource request using the provided configuration. -// The function combines the endpoint path with a resource ID and optional suffix to create a complete URL template. -// For example, if the config contains: -// - Endpoint: "/deviceManagement/configurationPolicies" -// - ResourceIDPattern: "('id')" -// - ResourceID: "12345" -// - EndpointSuffix: "/settings" -// -// The resulting template would be: "{+baseurl}/deviceManagement/configurationPolicies('12345')/settings" -// -// Parameters: -// - reqConfig: CustomGetRequestConfig containing the endpoint path, resource ID pattern, actual ID, and optional suffix -// -// Returns: -// - string: The constructed URL template ready for use with the Kiota request adapter -func ByIDRequestUrlTemplate(reqConfig CustomGetRequestConfig) string { - idFormat := strings.ReplaceAll(reqConfig.ResourceIDPattern, "id", reqConfig.ResourceID) - endpoint := reqConfig.Endpoint + idFormat - if reqConfig.EndpointSuffix != "" { - endpoint += reqConfig.EndpointSuffix - } - return "{+baseurl}" + endpoint -} - // 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. diff --git a/internal/client/custom_post_request.go b/internal/client/graphcustom/post_request.go similarity index 83% rename from internal/client/custom_post_request.go rename to internal/client/graphcustom/post_request.go index f490b1a4..2244e807 100644 --- a/internal/client/custom_post_request.go +++ b/internal/client/graphcustom/post_request.go @@ -1,4 +1,4 @@ -package client +package graphcustom import ( "context" @@ -8,8 +8,8 @@ import ( s "github.com/microsoft/kiota-abstractions-go/serialization" ) -// CustomPostRequestConfig contains the configuration for a custom POST request -type CustomPostRequestConfig struct { +// PostRequestConfig contains the configuration for a custom POST request +type PostRequestConfig struct { // The API version to use (beta or v1.0) APIVersion GraphAPIVersion // The base endpoint (e.g., "/deviceManagement/configurationPolicies") @@ -20,14 +20,14 @@ type CustomPostRequestConfig struct { QueryParameters map[string]string } -// SendCustomPostRequest performs a custom POST request using the Microsoft Graph SDK when the operation +// PostRequest performs a custom POST request using the Microsoft Graph SDK when the operation // is not available in the generated SDK methods. This function supports both Beta and V1.0 Graph API versions // and returns the parsed response model. // // Parameters: // - ctx: The context for the request, which can be used for cancellation and timeout // - adapter: The RequestAdapter interface for making HTTP requests -// - config: CustomPostRequestConfig containing: +// - config: PostRequestConfig containing: // - APIVersion: The Graph API version to use (Beta or V1.0) // - Endpoint: The resource endpoint path // - RequestBody: The body of the POST request implementing serialization.Parsable @@ -38,10 +38,10 @@ type CustomPostRequestConfig struct { // Returns: // - s.Parsable: The parsed response model // - error: Any error that occurred during the request -func SendCustomPostRequest( +func PostRequest( ctx context.Context, adapter abstractions.RequestAdapter, - config CustomPostRequestConfig, + config PostRequestConfig, factory s.ParsableFactory, errorMappings abstractions.ErrorMappings, ) (s.Parsable, error) { @@ -74,16 +74,16 @@ func SendCustomPostRequest( return result, nil } -// SendCustomPostRequestNoContent performs a custom POST request that doesn't expect a response body. +// PostRequestNoContent performs a custom POST request that doesn't expect a response body. // This is useful for operations that return 204 No Content. // -// Parameters are the same as SendCustomPostRequest except it doesn't take a responseModel parameter +// Parameters are the same as PostRequest except it doesn't take a responseModel parameter // and uses the SendNoContent method of the adapter. // // Returns: // - error: Returns nil if the request was successful (204 No Content received), // otherwise returns an error describing what went wrong -func SendCustomPostRequestNoContent(ctx context.Context, adapter abstractions.RequestAdapter, config CustomPostRequestConfig) error { +func PostRequestNoContent(ctx context.Context, adapter abstractions.RequestAdapter, config PostRequestConfig) error { requestInfo := abstractions.NewRequestInformation() requestInfo.Method = abstractions.POST requestInfo.UrlTemplate = "{+baseurl}/" + config.Endpoint diff --git a/internal/client/custom_put_request.go b/internal/client/graphcustom/put_request.go similarity index 85% rename from internal/client/custom_put_request.go rename to internal/client/graphcustom/put_request.go index 23d7719d..8d9a15e1 100644 --- a/internal/client/custom_put_request.go +++ b/internal/client/graphcustom/put_request.go @@ -1,4 +1,4 @@ -package client +package graphcustom import ( "context" @@ -16,8 +16,8 @@ const ( GraphAPIV1 GraphAPIVersion = "v1.0" ) -// CustomPutRequestConfig contains the configuration for a custom PUT request -type CustomPutRequestConfig struct { +// PutRequestConfig contains the configuration for a custom PUT request +type PutRequestConfig struct { // The API version to use (beta or v1.0) APIVersion GraphAPIVersion // The base endpoint (e.g., "/deviceManagement/configurationPolicies") @@ -28,7 +28,7 @@ type CustomPutRequestConfig struct { RequestBody s.Parsable } -// SendCustomPutRequestByResourceId performs a custom PUT request using the Microsoft Graph SDK when the operation +// PutRequestByResourceId performs a custom PUT request using the Microsoft Graph SDK when the operation // is not available in the generated SDK methods. This function supports both Beta and V1.0 Graph API versions // and expects a 204 No Content response from the server on success. // @@ -61,8 +61,8 @@ type CustomPutRequestConfig struct { // ResourceID: "d557c813-b8e5-4efc-b00e-9c0bd5fd10df", // RequestBody: myRequestBody, // } -// err := SendCustomPutRequestByResourceId(ctx, clients, config) -func SendCustomPutRequestByResourceId(ctx context.Context, adapter abstractions.RequestAdapter, config CustomPutRequestConfig) error { +// err := PutRequestByResourceId(ctx, clients, config) +func PutRequestByResourceId(ctx context.Context, adapter abstractions.RequestAdapter, config PutRequestConfig) error { requestInfo := abstractions.NewRequestInformation() requestInfo.Method = abstractions.PUT requestInfo.UrlTemplate = "{+baseurl}" + config.Endpoint + "('{id}')" diff --git a/internal/client/graphcustom/url_templates.go b/internal/client/graphcustom/url_templates.go new file mode 100644 index 00000000..aa2c6a5d --- /dev/null +++ b/internal/client/graphcustom/url_templates.go @@ -0,0 +1,27 @@ +package graphcustom + +import "strings" + +// ByIDRequestUrlTemplate constructs a URL template for a single resource request using the provided configuration. +// The function combines the endpoint path with a resource ID and optional suffix to create a complete URL template. +// For example, if the config contains: +// - Endpoint: "/deviceManagement/configurationPolicies" +// - ResourceIDPattern: "('id')" +// - ResourceID: "12345" +// - EndpointSuffix: "/settings" +// +// The resulting template would be: "{+baseurl}/deviceManagement/configurationPolicies('12345')/settings" +// +// Parameters: +// - reqConfig: GetRequestConfig containing the endpoint path, resource ID pattern, actual ID, and optional suffix +// +// Returns: +// - string: The constructed URL template ready for use with the Kiota request adapter +func ByIDRequestUrlTemplate(reqConfig GetRequestConfig) string { + idFormat := strings.ReplaceAll(reqConfig.ResourceIDPattern, "id", reqConfig.ResourceID) + endpoint := reqConfig.Endpoint + idFormat + if reqConfig.EndpointSuffix != "" { + endpoint += reqConfig.EndpointSuffix + } + return "{+baseurl}" + endpoint +} diff --git a/internal/resources/device_and_app_management/beta/endpoint_privilege_management/crud.go b/internal/resources/device_and_app_management/beta/endpoint_privilege_management/crud.go index 9404b253..73173e42 100644 --- a/internal/resources/device_and_app_management/beta/endpoint_privilege_management/crud.go +++ b/internal/resources/device_and_app_management/beta/endpoint_privilege_management/crud.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/deploymenttheory/terraform-provider-microsoft365/internal/client" + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/client/graphcustom" "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/crud" "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/errors" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -276,14 +276,14 @@ func (r *EndpointPrivilegeManagementResource) Update(ctx context.Context, req re return } - putRequest := client.CustomPutRequestConfig{ - APIVersion: client.GraphAPIBeta, + putRequest := graphcustom.PutRequestConfig{ + APIVersion: graphcustom.GraphAPIBeta, Endpoint: "deviceManagement/configurationPolicies", ResourceID: object.ID.ValueString(), RequestBody: requestBody, } - err = client.SendCustomPutRequestByResourceId(ctx, r.client.GetAdapter(), putRequest) + err = graphcustom.PutRequestByResourceId(ctx, r.client.GetAdapter(), putRequest) if err != nil { errors.HandleGraphError(ctx, err, resp, "Update", r.ReadPermissions) return diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go index 40d5f82a..37120e14 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/deploymenttheory/terraform-provider-microsoft365/internal/client" + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/client/graphcustom" "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/crud" "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/errors" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -109,8 +109,8 @@ func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.Creat } MapRemoteResourceStateToTerraform(ctx, &object, respResource) - settingsConfig := client.CustomGetRequestConfig{ - APIVersion: client.GraphAPIBeta, + settingsConfig := graphcustom.GetRequestConfig{ + APIVersion: graphcustom.GraphAPIBeta, Endpoint: r.ResourcePath, EndpointSuffix: "/settings", ResourceIDPattern: "('id')", @@ -120,7 +120,7 @@ func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.Creat }, } - respSettings, err := client.SendCustomGetRequestByResourceId( + respSettings, err := graphcustom.GetRequestByResourceId( ctx, r.client.GetAdapter(), settingsConfig, @@ -198,8 +198,8 @@ func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadReq MapRemoteResourceStateToTerraform(ctx, &object, respResource) - settingsConfig := client.CustomGetRequestConfig{ - APIVersion: client.GraphAPIBeta, + settingsConfig := graphcustom.GetRequestConfig{ + APIVersion: graphcustom.GraphAPIBeta, Endpoint: r.ResourcePath, EndpointSuffix: "/settings", ResourceIDPattern: "('id')", @@ -209,7 +209,7 @@ func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadReq }, } - respSettings, err := client.SendCustomGetRequestByResourceId( + respSettings, err := graphcustom.GetRequestByResourceId( ctx, r.client.GetAdapter(), settingsConfig, @@ -281,14 +281,14 @@ func (r *SettingsCatalogResource) Update(ctx context.Context, req resource.Updat return } - putRequest := client.CustomPutRequestConfig{ - APIVersion: client.GraphAPIBeta, + putRequest := graphcustom.PutRequestConfig{ + APIVersion: graphcustom.GraphAPIBeta, Endpoint: r.ResourcePath, ResourceID: object.ID.ValueString(), RequestBody: requestBody, } - err = client.SendCustomPutRequestByResourceId(ctx, r.client.GetAdapter(), putRequest) + err = graphcustom.PutRequestByResourceId(ctx, r.client.GetAdapter(), putRequest) if err != nil { errors.HandleGraphError(ctx, err, resp, "Update", r.ReadPermissions) return From 22bfe8da59818910f7391fb91febc33b02e1b43f Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 12:07:12 +0000 Subject: [PATCH 04/13] Add unit tests for ByIDRequestUrlTemplate function in graphcustom package; validate various endpoint configurations and expected URL outputs to ensure correct URL construction. --- .../client/graphcustom/url_templates_test.go | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 internal/client/graphcustom/url_templates_test.go diff --git a/internal/client/graphcustom/url_templates_test.go b/internal/client/graphcustom/url_templates_test.go new file mode 100644 index 00000000..5fa9c4f5 --- /dev/null +++ b/internal/client/graphcustom/url_templates_test.go @@ -0,0 +1,117 @@ +package graphcustom + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestByIDRequestUrlTemplate(t *testing.T) { + tests := []struct { + name string + config GetRequestConfig + expected string + }{ + { + name: "basic endpoint with ID pattern", + config: GetRequestConfig{ + Endpoint: "/deviceManagement/configurationPolicies", + ResourceIDPattern: "('id')", + ResourceID: "12345", + }, + expected: "{+baseurl}/deviceManagement/configurationPolicies('12345')", + }, + { + name: "endpoint with ID pattern and suffix", + config: GetRequestConfig{ + Endpoint: "/deviceManagement/configurationPolicies", + ResourceIDPattern: "('id')", + ResourceID: "12345", + EndpointSuffix: "/settings", + }, + expected: "{+baseurl}/deviceManagement/configurationPolicies('12345')/settings", + }, + { + name: "endpoint without leading slash", + config: GetRequestConfig{ + Endpoint: "deviceManagement/configurationPolicies", + ResourceIDPattern: "('id')", + ResourceID: "12345", + }, + expected: "{+baseurl}deviceManagement/configurationPolicies('12345')", + }, + { + name: "endpoint with different ID pattern format", + config: GetRequestConfig{ + Endpoint: "/users", + ResourceIDPattern: "(id)", + ResourceID: "user@contoso.com", + }, + expected: "{+baseurl}/users(user@contoso.com)", + }, + { + name: "complex ID with special characters", + config: GetRequestConfig{ + Endpoint: "/deviceManagement/configurationPolicies", + ResourceIDPattern: "('id')", + ResourceID: "12345-67890-abcdef", + }, + expected: "{+baseurl}/deviceManagement/configurationPolicies('12345-67890-abcdef')", + }, + { + name: "endpoint with multiple path segments and suffix", + config: GetRequestConfig{ + Endpoint: "/users/mailFolders/messages", + ResourceIDPattern: "('id')", + ResourceID: "ABC123", + EndpointSuffix: "/attachments", + }, + expected: "{+baseurl}/users/mailFolders/messages('ABC123')/attachments", + }, + { + name: "empty suffix", + config: GetRequestConfig{ + Endpoint: "/deviceManagement/configurationPolicies", + ResourceIDPattern: "('id')", + ResourceID: "12345", + EndpointSuffix: "", + }, + expected: "{+baseurl}/deviceManagement/configurationPolicies('12345')", + }, + { + name: "suffix without leading slash", + config: GetRequestConfig{ + Endpoint: "/deviceManagement/configurationPolicies", + ResourceIDPattern: "('id')", + ResourceID: "12345", + EndpointSuffix: "settings", + }, + expected: "{+baseurl}/deviceManagement/configurationPolicies('12345')settings", + }, + { + name: "GUID in resource ID", + config: GetRequestConfig{ + Endpoint: "/deviceManagement/configurationPolicies", + ResourceIDPattern: "('id')", + ResourceID: "d557c813-b8e5-4efc-b00e-9c0bd5fd10df", + }, + expected: "{+baseurl}/deviceManagement/configurationPolicies('d557c813-b8e5-4efc-b00e-9c0bd5fd10df')", + }, + { + name: "empty resource ID", + config: GetRequestConfig{ + Endpoint: "/deviceManagement/configurationPolicies", + ResourceIDPattern: "('id')", + ResourceID: "", + }, + expected: "{+baseurl}/deviceManagement/configurationPolicies('')", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ByIDRequestUrlTemplate(tt.config) + assert.Equal(t, tt.expected, result, "URL template should match expected value") + }) + } +} From a40cddf96640d9eb708d85bbd67809e2a4c10945 Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 12:32:28 +0000 Subject: [PATCH 05/13] Refactor error handling in request functions; improve error messages for clarity in GetRequestByResourceId, PostRequest, and PutRequestByResourceId. Introduce PutResponse struct for better response management. --- internal/client/graphcustom/get_request.go | 4 ++-- internal/client/graphcustom/post_request.go | 4 ++-- internal/client/graphcustom/put_request.go | 7 ++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/internal/client/graphcustom/get_request.go b/internal/client/graphcustom/get_request.go index a7cb4799..f47d14aa 100644 --- a/internal/client/graphcustom/get_request.go +++ b/internal/client/graphcustom/get_request.go @@ -179,7 +179,7 @@ func GetRequestByResourceId(ctx context.Context, adapter abstractions.RequestAda 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 request: %w", err) + return nil, fmt.Errorf("error converting to native HTTP request: %w", err) } httpReq := nativeReq.(*http.Request) @@ -191,7 +191,7 @@ func makeRequest(ctx context.Context, adapter abstractions.RequestAdapter, reque resp, err := client.Do(httpReq) if err != nil { - return nil, fmt.Errorf("error executing request: %w", err) + return nil, fmt.Errorf("error executing GET request: %w", err) } defer resp.Body.Close() diff --git a/internal/client/graphcustom/post_request.go b/internal/client/graphcustom/post_request.go index 2244e807..df9c96d1 100644 --- a/internal/client/graphcustom/post_request.go +++ b/internal/client/graphcustom/post_request.go @@ -68,7 +68,7 @@ func PostRequest( // Send the request using the adapter's Send method result, err := adapter.Send(ctx, requestInfo, factory, errorMappings) if err != nil { - return nil, fmt.Errorf("error sending request: %v", err) + return nil, err } return result, nil @@ -106,7 +106,7 @@ func PostRequestNoContent(ctx context.Context, adapter abstractions.RequestAdapt // Use SendNoContent for requests that don't return a response body err = adapter.SendNoContent(ctx, requestInfo, nil) if err != nil { - return fmt.Errorf("error sending post request: %v", err) + return err } return nil diff --git a/internal/client/graphcustom/put_request.go b/internal/client/graphcustom/put_request.go index 8d9a15e1..a24af713 100644 --- a/internal/client/graphcustom/put_request.go +++ b/internal/client/graphcustom/put_request.go @@ -28,6 +28,11 @@ type PutRequestConfig struct { RequestBody s.Parsable } +type PutResponse struct { + StatusCode int + Error error +} + // PutRequestByResourceId performs a custom PUT request using the Microsoft Graph SDK when the operation // is not available in the generated SDK methods. This function supports both Beta and V1.0 Graph API versions // and expects a 204 No Content response from the server on success. @@ -78,7 +83,7 @@ func PutRequestByResourceId(ctx context.Context, adapter abstractions.RequestAda err = adapter.SendNoContent(ctx, requestInfo, nil) if err != nil { - return fmt.Errorf("error sending request: %v", err) + return err } return nil From 9390c3031457cc34c80496ff848be2211c3f16de Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 12:37:00 +0000 Subject: [PATCH 06/13] Simplify error handling in makeRequest function; return original error for GET request execution and response reading failures to improve clarity and maintainability. --- internal/client/graphcustom/get_request.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/client/graphcustom/get_request.go b/internal/client/graphcustom/get_request.go index f47d14aa..bd694e5e 100644 --- a/internal/client/graphcustom/get_request.go +++ b/internal/client/graphcustom/get_request.go @@ -191,13 +191,13 @@ func makeRequest(ctx context.Context, adapter abstractions.RequestAdapter, reque resp, err := client.Do(httpReq) if err != nil { - return nil, fmt.Errorf("error executing GET request: %w", err) + return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("error reading response: %w", err) + return nil, err } return body, nil From d4f72bb9b12963fa66dd33886c006766ad8f0b0b Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 13:00:41 +0000 Subject: [PATCH 07/13] Refactor Create method in SettingsCatalogResource; streamline state management by directly appending diagnostics and updating state from Read operation. Enhance MapRemoteSettingsStateToTerraform function to normalize settings JSON and preserve secret values, improving error handling and logging. --- .../endpoint_privilege_management/crud.go | 107 +++++++----------- .../state_settings.go | 64 ++++++++++- .../beta/settings_catalog/crud.go | 64 +++-------- 3 files changed, 112 insertions(+), 123 deletions(-) diff --git a/internal/resources/device_and_app_management/beta/endpoint_privilege_management/crud.go b/internal/resources/device_and_app_management/beta/endpoint_privilege_management/crud.go index 73173e42..17e72cdd 100644 --- a/internal/resources/device_and_app_management/beta/endpoint_privilege_management/crud.go +++ b/internal/resources/device_and_app_management/beta/endpoint_privilege_management/crud.go @@ -12,7 +12,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - msgraphsdk "github.com/microsoftgraph/msgraph-beta-sdk-go/devicemanagement" ) var ( @@ -23,17 +22,18 @@ var ( object EndpointPrivilegeManagementResourceModel ) -// Create handles the Create operation for Endpoint Privilege Management resources. +// Create handles the Create operation for Settings Catalog resources. // // - Retrieves the planned configuration from the create request // - Constructs the resource request body from the plan // - Sends POST request to create the base resource and settings // - Captures the new resource ID from the response // - Constructs and sends assignment configuration if specified -// - Maps the created resource state to Terraform -// - Updates the final state with all resource data +// - Sets initial state with planned values +// - Calls Read operation to fetch the latest state from the API +// - Updates the final state with the fresh data from the API // -// The function ensures that both the Endpoint Privilege Management profile and its assignments +// The function ensures that both the settings catalog profile and its assignments // (if specified) are created properly. The settings must be defined during creation // as they are required for a successful deployment, while assignments are optional. func (r *EndpointPrivilegeManagementResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { @@ -98,60 +98,30 @@ func (r *EndpointPrivilegeManagementResource) Create(ctx context.Context, req re } } - respResource, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Get(context.Background(), nil) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Create", r.WritePermissions) - return - } - MapRemoteResourceStateToTerraform(ctx, &object, respResource) - - respSettings, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Settings(). - Get(context.Background(), &msgraphsdk.ConfigurationPoliciesItemSettingsRequestBuilderGetRequestConfiguration{ - QueryParameters: &msgraphsdk.ConfigurationPoliciesItemSettingsRequestBuilderGetQueryParameters{ - Expand: []string{""}, - }, - }) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Create - Settings Fetch", r.ReadPermissions) + resp.Diagnostics.Append(resp.State.Set(ctx, &object)...) + if resp.Diagnostics.HasError() { return } - settingsList := respSettings.GetValue() - MapRemoteSettingsStateToTerraform(ctx, &object, settingsList) - - respAssignments, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Assignments(). - Get(context.Background(), nil) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Create - Assignments Fetch", r.ReadPermissions) - return + readResp := &resource.ReadResponse{ + State: resp.State, } + r.Read(ctx, resource.ReadRequest{ + State: resp.State, + ProviderMeta: req.ProviderMeta, + }, readResp) - MapRemoteAssignmentStateToTerraform(ctx, &object, respAssignments) - - resp.Diagnostics.Append(resp.State.Set(ctx, &object)...) + resp.Diagnostics.Append(readResp.Diagnostics...) if resp.Diagnostics.HasError() { return } - tflog.Debug(ctx, fmt.Sprintf("Finished Create Method: %s_%s", r.ProviderTypeName, r.TypeName)) + resp.State = readResp.State + + tflog.Debug(ctx, fmt.Sprintf("Finished Update Method: %s_%s", r.ProviderTypeName, r.TypeName)) } -// Read handles the Read operation for Endpoint Privilege Management resources. +// Read handles the Read operation for Settings Catalog resources. // // - Retrieves the current state from the read request // - Gets the base resource details from the API @@ -160,7 +130,6 @@ func (r *EndpointPrivilegeManagementResource) Create(ctx context.Context, req re // - Maps the settings configuration to Terraform state // - Gets the assignments configuration from the API // - Maps the assignments configuration to Terraform state -// - Updates the final Terraform state with all mapped data // // The function ensures that all components (base resource, settings, and assignments) // are properly read and mapped into the Terraform state, providing a complete view @@ -195,27 +164,29 @@ func (r *EndpointPrivilegeManagementResource) Read(ctx context.Context, req reso MapRemoteResourceStateToTerraform(ctx, &object, respResource) - // Retrieve settings from the response - respSettings, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Settings(). - Get(context.Background(), &msgraphsdk.ConfigurationPoliciesItemSettingsRequestBuilderGetRequestConfiguration{ - QueryParameters: &msgraphsdk.ConfigurationPoliciesItemSettingsRequestBuilderGetQueryParameters{ - Expand: []string{""}, // Expand all related settings - }, - }) + settingsConfig := graphcustom.GetRequestConfig{ + APIVersion: graphcustom.GraphAPIBeta, + Endpoint: r.ResourcePath, + EndpointSuffix: "/settings", + ResourceIDPattern: "('id')", + ResourceID: object.ID.ValueString(), + QueryParameters: map[string]string{ + "$expand": "children", + }, + } + + respSettings, err := graphcustom.GetRequestByResourceId( + ctx, + r.client.GetAdapter(), + settingsConfig, + ) if err != nil { - errors.HandleGraphError(ctx, err, resp, "Read", r.ReadPermissions) + errors.HandleGraphError(ctx, err, resp, "Create - Settings Fetch", r.ReadPermissions) return } - // Extract the list of settings from the collection response - settingsList := respSettings.GetValue() - - MapRemoteSettingsStateToTerraform(ctx, &object, settingsList) + MapRemoteSettingsStateToTerraform(ctx, &object, respSettings) respAssignments, err := r.client. DeviceManagement(). @@ -239,7 +210,7 @@ func (r *EndpointPrivilegeManagementResource) Read(ctx context.Context, req reso tflog.Debug(ctx, fmt.Sprintf("Finished Read Method: %s_%s", r.ProviderTypeName, r.TypeName)) } -// Update handles the Update operation for Endpoint Privilege Management resources. +// Update handles the Update operation for Settings Catalog resources. // // - Retrieves the planned changes from the update request // - Constructs the resource request body from the plan @@ -278,7 +249,7 @@ func (r *EndpointPrivilegeManagementResource) Update(ctx context.Context, req re putRequest := graphcustom.PutRequestConfig{ APIVersion: graphcustom.GraphAPIBeta, - Endpoint: "deviceManagement/configurationPolicies", + Endpoint: r.ResourcePath, ResourceID: object.ID.ValueString(), RequestBody: requestBody, } @@ -333,7 +304,7 @@ func (r *EndpointPrivilegeManagementResource) Update(ctx context.Context, req re tflog.Debug(ctx, fmt.Sprintf("Finished Update Method: %s_%s", r.ProviderTypeName, r.TypeName)) } -// Delete handles the Delete operation for Endpoint Privilege Management resources. +// Delete handles the Delete operation for Settings Catalog resources. // // - Retrieves the current state from the delete request // - Validates the state data and timeout configuration diff --git a/internal/resources/device_and_app_management/beta/endpoint_privilege_management/state_settings.go b/internal/resources/device_and_app_management/beta/endpoint_privilege_management/state_settings.go index af4a8cb9..b36a1e39 100644 --- a/internal/resources/device_and_app_management/beta/endpoint_privilege_management/state_settings.go +++ b/internal/resources/device_and_app_management/beta/endpoint_privilege_management/state_settings.go @@ -2,18 +2,70 @@ package graphBetaEndpointPrivilegeManagement import ( "context" + "encoding/json" + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/normalize" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - graphmodels "github.com/microsoftgraph/msgraph-beta-sdk-go/models" ) -func MapRemoteSettingsStateToTerraform(ctx context.Context, data *EndpointPrivilegeManagementResourceModel, remoteSettings []graphmodels.DeviceManagementConfigurationSettingable) { - if remoteSettings == nil { - tflog.Debug(ctx, "Remote settings are nil") +// MapRemoteSettingsStateToTerraform maps the remote settings catalog settings state to the Terraform state +// taking the raw json from a custom GET request, normalizing the content and then stating. The stating logic: +// 1. Parses the original HCL settings to preserve secret values and states +// 2. Parses the raw response and extracts the settings content +// 3. Structures the content under settingsDetails like the PUT request +// 4. Recursively preserves secret setting values and states from the original HCL config +// 5. Converts the structured content to JSON and normalizes it alphabetically +// 6. States the normalized JSON in the Terraform state +func MapRemoteSettingsStateToTerraform(ctx context.Context, data *EndpointPrivilegeManagementResourceModel, resp []byte) { + var configSettings map[string]interface{} + if err := json.Unmarshal([]byte(data.Settings.ValueString()), &configSettings); err != nil { + tflog.Error(ctx, "Failed to unmarshal config settings", map[string]interface{}{"error": err.Error()}) return } - tflog.Debug(ctx, "Starting to map settings state to Terraform state") + var rawResponse map[string]interface{} + if err := json.Unmarshal(resp, &rawResponse); err != nil { + var arrayResponse []interface{} + if err := json.Unmarshal(resp, &arrayResponse); err != nil { + tflog.Error(ctx, "Failed to unmarshal settings response", map[string]interface{}{"error": err.Error()}) + return + } + rawResponse = map[string]interface{}{"value": arrayResponse} + } + + var settingsContent interface{} + if value, ok := rawResponse["value"]; ok { + settingsContent = value + } else if details, ok := rawResponse["settingsDetails"]; ok { + settingsContent = details + } else { + settingsContent = rawResponse + } + + structuredContent := map[string]interface{}{ + "settingsDetails": settingsContent, + } + + if err := normalize.PreserveSecretSettings(configSettings, structuredContent); err != nil { + tflog.Error(ctx, "Error stating settings catalog secret settings from HCL", map[string]interface{}{"error": err.Error()}) + return + } + + jsonBytes, err := json.Marshal(structuredContent) + if err != nil { + tflog.Error(ctx, "Failed to marshal JSON structured content during preparation for normalization", map[string]interface{}{"error": err.Error()}) + return + } + + normalizedJSON, err := normalize.JSONAlphabetically(string(jsonBytes)) + if err != nil { + tflog.Error(ctx, "Failed to normalize settings catalog JSON alphabetically", map[string]interface{}{"error": err.Error()}) + return + } + + tflog.Debug(ctx, "Original settings", map[string]interface{}{"settings": string(resp)}) + tflog.Debug(ctx, "Normalized settings", map[string]interface{}{"settings": normalizedJSON}) - tflog.Debug(ctx, "Finished mapping settings state to Terraform state") + data.Settings = types.StringValue(normalizedJSON) } diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go index 37120e14..3ab86cb8 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go @@ -29,8 +29,9 @@ var ( // - Sends POST request to create the base resource and settings // - Captures the new resource ID from the response // - Constructs and sends assignment configuration if specified -// - Maps the created resource state to Terraform -// - Updates the final state with all resource data +// - Sets initial state with planned values +// - Calls Read operation to fetch the latest state from the API +// - Updates the final state with the fresh data from the API // // The function ensures that both the settings catalog profile and its assignments // (if specified) are created properly. The settings must be defined during creation @@ -97,62 +98,27 @@ func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.Creat } } - respResource, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Get(context.Background(), nil) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Create", r.WritePermissions) - return - } - MapRemoteResourceStateToTerraform(ctx, &object, respResource) - - settingsConfig := graphcustom.GetRequestConfig{ - APIVersion: graphcustom.GraphAPIBeta, - Endpoint: r.ResourcePath, - EndpointSuffix: "/settings", - ResourceIDPattern: "('id')", - ResourceID: object.ID.ValueString(), - QueryParameters: map[string]string{ - "$expand": "children", - }, - } - - respSettings, err := graphcustom.GetRequestByResourceId( - ctx, - r.client.GetAdapter(), - settingsConfig, - ) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Create - Settings Fetch", r.ReadPermissions) + resp.Diagnostics.Append(resp.State.Set(ctx, &object)...) + if resp.Diagnostics.HasError() { return } - MapRemoteSettingsStateToTerraform(ctx, &object, respSettings) - - respAssignments, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Assignments(). - Get(context.Background(), nil) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Create - Assignments Fetch", r.ReadPermissions) - return + readResp := &resource.ReadResponse{ + State: resp.State, } + r.Read(ctx, resource.ReadRequest{ + State: resp.State, + ProviderMeta: req.ProviderMeta, + }, readResp) - MapRemoteAssignmentStateToTerraform(ctx, &object, respAssignments) - - resp.Diagnostics.Append(resp.State.Set(ctx, &object)...) + resp.Diagnostics.Append(readResp.Diagnostics...) if resp.Diagnostics.HasError() { return } - tflog.Debug(ctx, fmt.Sprintf("Finished Create Method: %s_%s", r.ProviderTypeName, r.TypeName)) + resp.State = readResp.State + + tflog.Debug(ctx, fmt.Sprintf("Finished Update Method: %s_%s", r.ProviderTypeName, r.TypeName)) } // Read handles the Read operation for Settings Catalog resources. From 5c533d90ff20ad4120f62f8277a7eb8ede72bd6d Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 14:30:17 +0000 Subject: [PATCH 08/13] Enhance error handling for rate limits; introduce RetryableAssignmentOperation and RetryableOperation functions to manage throttling and implement exponential backoff with jitter. Update state mapping in settings catalog to remove unused fields, improving clarity and maintainability. --- .../resources/common/errors/error_handling.go | 25 ++ .../common/retry/retry_assignments.go | 97 +++++ .../resources/common/retry/retry_global.go | 114 ++++++ .../resources/common/retry/retry_intune.go | 121 ++++++ .../beta/settings_catalog/crud.go | 131 ++++--- .../beta/settings_catalog/crud.go.back | 344 ++++++++++++++++++ .../beta/settings_catalog/model.go | 269 +------------- .../beta/settings_catalog/resource_v7.go | 45 ++- .../settings_catalog/state_base_resource.go | 8 +- 9 files changed, 820 insertions(+), 334 deletions(-) create mode 100644 internal/resources/common/retry/retry_assignments.go create mode 100644 internal/resources/common/retry/retry_global.go create mode 100644 internal/resources/common/retry/retry_intune.go create mode 100644 internal/resources/device_and_app_management/beta/settings_catalog/crud.go.back diff --git a/internal/resources/common/errors/error_handling.go b/internal/resources/common/errors/error_handling.go index f59df15d..a55a8a6f 100644 --- a/internal/resources/common/errors/error_handling.go +++ b/internal/resources/common/errors/error_handling.go @@ -26,6 +26,7 @@ type GraphErrorInfo struct { AdditionalData map[string]interface{} Headers *abstractions.ResponseHeaders RequestDetails string + RetryAfter string } // standardErrorDescriptions provides consistent error messaging across the provider @@ -85,6 +86,9 @@ func HandleGraphError(ctx context.Context, err error, resp interface{}, operatio case 401, 403: handlePermissionError(ctx, errorInfo, resp, operation, requiredPermissions) + case 429: + handleRateLimitError(ctx, errorInfo, resp) + default: // Handle all other cases addErrorToDiagnostics(ctx, resp, errorDesc.Summary, @@ -216,6 +220,27 @@ func handlePermissionError(ctx context.Context, errorInfo GraphErrorInfo, resp i addErrorToDiagnostics(ctx, resp, errorDesc.Summary, detail) } +// handleRateLimitError processes rate limit errors and adds retry information to the error message +func handleRateLimitError(ctx context.Context, errorInfo GraphErrorInfo, resp interface{}) GraphErrorInfo { + if headers := errorInfo.Headers; headers != nil { + retryValues := headers.Get("Retry-After") + if len(retryValues) > 0 { + errorInfo.RetryAfter = retryValues[0] + } + } + + tflog.Warn(ctx, "Rate limit exceeded", map[string]interface{}{ + "retry_after": errorInfo.RetryAfter, + "details": errorInfo.ErrorMessage, + }) + + errorDesc := getErrorDescription(429) + detail := constructErrorDetail(errorDesc.Detail, errorInfo.ErrorMessage) + addErrorToDiagnostics(ctx, resp, errorDesc.Summary, detail) + + return errorInfo +} + // addErrorToDiagnostics adds an error to the response diagnostics func addErrorToDiagnostics(ctx context.Context, resp interface{}, summary, detail string) { switch r := resp.(type) { diff --git a/internal/resources/common/retry/retry_assignments.go b/internal/resources/common/retry/retry_assignments.go new file mode 100644 index 00000000..9920aba3 --- /dev/null +++ b/internal/resources/common/retry/retry_assignments.go @@ -0,0 +1,97 @@ +// REF: https://learn.microsoft.com/en-us/graph/throttling-limits#assignment-service-limits + +package retry + +import ( + "context" + "time" + + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/errors" + "github.com/hashicorp/terraform-plugin-log/tflog" + "golang.org/x/exp/rand" +) + +// RetryableAssignmentOperation executes an assignment operation with specific rate limiting +func RetryableAssignmentOperation(ctx context.Context, operation string, fn func() error) error { + var attempt int + r := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + + const ( + tenSecondLimit = 500 // requests per 10 seconds per app per tenant + hourlyLimit = 15000 // requests per hour per app per tenant + maxBackoff = 10 * time.Second + baseDelay = 3 * time.Second // Higher base delay for assignments + ) + + for { + err := fn() + if err == nil { + return nil + } + + graphError := errors.GraphError(ctx, err) + if graphError.StatusCode != 429 { + return err + } + + // Parse throttle scope if available + var throttleScope ThrottleScope + if scope := graphError.Headers.Get("x-ms-throttle-scope"); len(scope) > 0 { + throttleScope = parseThrottleScope(scope[0]) + } + + // Get throttle information + var throttleInfo string + if info := graphError.Headers.Get("x-ms-throttle-information"); len(info) > 0 { + throttleInfo = info[0] + } + + // Use Retry-After if provided, otherwise use exponential backoff + var backoffDelay time.Duration + if graphError.RetryAfter != "" { + if seconds, err := time.ParseDuration(graphError.RetryAfter + "s"); err == nil { + backoffDelay = seconds + } + } + + if backoffDelay == 0 { + backoffDelay = baseDelay * time.Duration(1< maxBackoff { + backoffDelay = maxBackoff + } + } + + // Add jitter: randomly between 50-100% of calculated delay + jitterDelay := backoffDelay/2 + time.Duration(r.Int63n(int64(backoffDelay/2))) + attempt++ + + logDetails := map[string]interface{}{ + "operation": operation, + "attempt": attempt, + "delay_seconds": jitterDelay.Seconds(), + "status_code": graphError.StatusCode, + "rate_limit_10s": tenSecondLimit, + "rate_limit_1h": hourlyLimit, + } + + if throttleInfo != "" { + logDetails["throttle_reason"] = throttleInfo + } + if throttleScope != (ThrottleScope{}) { + logDetails["throttle_scope"] = throttleScope.Scope + logDetails["throttle_limit"] = throttleScope.Limit + } + + tflog.Info(ctx, "Microsoft Graph assignment rate limit encountered", logDetails) + + timer := time.NewTimer(jitterDelay) + defer timer.Stop() + + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C: + continue + } + } +} diff --git a/internal/resources/common/retry/retry_global.go b/internal/resources/common/retry/retry_global.go new file mode 100644 index 00000000..9c348ceb --- /dev/null +++ b/internal/resources/common/retry/retry_global.go @@ -0,0 +1,114 @@ +// REF: https://learn.microsoft.com/en-us/graph/throttling + +package retry + +import ( + "context" + "strings" + "time" + + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/errors" + "github.com/hashicorp/terraform-plugin-log/tflog" + "golang.org/x/exp/rand" +) + +// ThrottleScope represents the scope of throttling from x-ms-throttle-scope header +type ThrottleScope struct { + Scope string + Limit string + ApplicationID string + ResourceID string +} + +// parseThrottleScope parses the x-ms-throttle-scope header +func parseThrottleScope(scope string) ThrottleScope { + parts := strings.Split(scope, "/") + if len(parts) != 4 { + return ThrottleScope{} + } + return ThrottleScope{ + Scope: parts[0], + Limit: parts[1], + ApplicationID: parts[2], + ResourceID: parts[3], + } +} + +// RetryableOperation executes an operation with automatic retry on rate limit errors +func RetryableOperation(ctx context.Context, operation string, fn func() error) error { + var attempt int + r := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + + for { + err := fn() + if err == nil { + return nil + } + + graphError := errors.GraphError(ctx, err) + if graphError.StatusCode != 429 { + return err + } + + // Parse throttle scope if available + var throttleScope ThrottleScope + if scope := graphError.Headers.Get("x-ms-throttle-scope"); len(scope) > 0 { + throttleScope = parseThrottleScope(scope[0]) + } + + // Get throttle information + var throttleInfo string + if info := graphError.Headers.Get("x-ms-throttle-information"); len(info) > 0 { + throttleInfo = info[0] + } + + const maxBackoff = 10 * time.Second + baseDelay := 2 * time.Second + + // Use Retry-After if provided, otherwise use exponential backoff + var backoffDelay time.Duration + if graphError.RetryAfter != "" { + if seconds, err := time.ParseDuration(graphError.RetryAfter + "s"); err == nil { + backoffDelay = seconds + } + } + + if backoffDelay == 0 { + backoffDelay = baseDelay * time.Duration(1< maxBackoff { + backoffDelay = maxBackoff + } + } + + // Add jitter: randomly between 50-100% of calculated delay + jitterDelay := backoffDelay/2 + time.Duration(r.Int63n(int64(backoffDelay/2))) + attempt++ + + logDetails := map[string]interface{}{ + "operation": operation, + "attempt": attempt, + "delay_seconds": jitterDelay.Seconds(), + "status_code": graphError.StatusCode, + } + + if throttleInfo != "" { + logDetails["throttle_reason"] = throttleInfo + } + if throttleScope != (ThrottleScope{}) { + logDetails["throttle_scope"] = throttleScope.Scope + logDetails["throttle_limit"] = throttleScope.Limit + } + + tflog.Info(ctx, "Microsoft Graph rate limit encountered", logDetails) + + timer := time.NewTimer(jitterDelay) + defer timer.Stop() + + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C: + continue + } + } +} diff --git a/internal/resources/common/retry/retry_intune.go b/internal/resources/common/retry/retry_intune.go new file mode 100644 index 00000000..5d425f3f --- /dev/null +++ b/internal/resources/common/retry/retry_intune.go @@ -0,0 +1,121 @@ +// REF: https://learn.microsoft.com/en-us/graph/throttling-limits#intune-service-limits + +package retry + +import ( + "context" + "time" + + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/errors" + "github.com/hashicorp/terraform-plugin-log/tflog" + "golang.org/x/exp/rand" +) + +// IntuneOperationType defines the type of Intune operation +type IntuneOperationType string + +const ( + IntuneWrite IntuneOperationType = "Write" // POST, PUT, DELETE, PATCH + IntuneRead IntuneOperationType = "Read" // GET and others +) + +// RetryableIntuneOperation executes an Intune operation with specific rate limiting +func RetryableIntuneOperation(ctx context.Context, operation string, opType IntuneOperationType, fn func() error) error { + var attempt int + r := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + + const ( + // Write operations (POST, PUT, DELETE, PATCH) + writePerAppLimit = 100 // requests per 20 seconds + writeTenantLimit = 200 // requests per 20 seconds + + // General operations + generalPerAppLimit = 1000 // requests per 20 seconds + generalTenantLimit = 2000 // requests per 20 seconds + + maxBackoff = 10 * time.Second + baseDelay = 2 * time.Second + ) + + for { + err := fn() + if err == nil { + return nil + } + + graphError := errors.GraphError(ctx, err) + if graphError.StatusCode != 429 { + return err + } + + // Parse throttle scope if available + var throttleScope ThrottleScope + if scope := graphError.Headers.Get("x-ms-throttle-scope"); len(scope) > 0 { + throttleScope = parseThrottleScope(scope[0]) + } + + // Get throttle information + var throttleInfo string + if info := graphError.Headers.Get("x-ms-throttle-information"); len(info) > 0 { + throttleInfo = info[0] + } + + // Use Retry-After if provided, otherwise use exponential backoff + var backoffDelay time.Duration + if graphError.RetryAfter != "" { + if seconds, err := time.ParseDuration(graphError.RetryAfter + "s"); err == nil { + backoffDelay = seconds + } + } + + if backoffDelay == 0 { + backoffDelay = baseDelay * time.Duration(1< maxBackoff { + backoffDelay = maxBackoff + } + } + + // Add jitter: randomly between 50-100% of calculated delay + jitterDelay := backoffDelay/2 + time.Duration(r.Int63n(int64(backoffDelay/2))) + attempt++ + + // Enhanced logging with rate limit context + logDetails := map[string]interface{}{ + "operation": operation, + "attempt": attempt, + "delay_seconds": jitterDelay.Seconds(), + "status_code": graphError.StatusCode, + "operation_type": string(opType), + } + + if opType == IntuneWrite { + logDetails["rate_limit_per_app"] = writePerAppLimit + logDetails["rate_limit_tenant"] = writeTenantLimit + logDetails["window_seconds"] = 20 + } else { + logDetails["rate_limit_per_app"] = generalPerAppLimit + logDetails["rate_limit_tenant"] = generalTenantLimit + logDetails["window_seconds"] = 20 + } + + if throttleInfo != "" { + logDetails["throttle_reason"] = throttleInfo + } + if throttleScope != (ThrottleScope{}) { + logDetails["throttle_scope"] = throttleScope.Scope + logDetails["throttle_limit"] = throttleScope.Limit + } + + tflog.Info(ctx, "Intune service rate limit encountered", logDetails) + + timer := time.NewTimer(jitterDelay) + defer timer.Stop() + + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C: + continue + } + } +} diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go index 3ab86cb8..75e4127e 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go @@ -9,6 +9,7 @@ import ( "github.com/deploymenttheory/terraform-provider-microsoft365/internal/client/graphcustom" "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/crud" "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/errors" + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/retry" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -37,7 +38,6 @@ var ( // (if specified) are created properly. The settings must be defined during creation // as they are required for a successful deployment, while assignments are optional. func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - mu.Lock() defer mu.Unlock() @@ -63,17 +63,21 @@ func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.Creat return } - requestResource, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - Post(context.Background(), requestBody, nil) + err = retry.RetryableIntuneOperation(ctx, "create resource", retry.IntuneWrite, func() error { + var opErr error + requestBody, opErr = r.client. + DeviceManagement(). + ConfigurationPolicies(). + Post(context.Background(), requestBody, nil) + return opErr + }) if err != nil { errors.HandleGraphError(ctx, err, resp, "Create", r.WritePermissions) return } - object.ID = types.StringValue(*requestResource.GetId()) + object.ID = types.StringValue(*requestBody.GetId()) if object.Assignments != nil { requestAssignment, err := constructAssignment(ctx, &object) @@ -85,12 +89,15 @@ func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.Creat return } - _, err = r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Assign(). - Post(ctx, requestAssignment, nil) + err = retry.RetryableAssignmentOperation(ctx, "create assignment", func() error { + _, err := r.client. + DeviceManagement(). + ConfigurationPolicies(). + ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). + Assign(). + Post(ctx, requestAssignment, nil) + return err + }) if err != nil { errors.HandleGraphError(ctx, err, resp, "Create", r.WritePermissions) @@ -118,7 +125,7 @@ func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.Creat resp.State = readResp.State - tflog.Debug(ctx, fmt.Sprintf("Finished Update Method: %s_%s", r.ProviderTypeName, r.TypeName)) + tflog.Debug(ctx, fmt.Sprintf("Finished Create Method: %s_%s", r.ProviderTypeName, r.TypeName)) } // Read handles the Read operation for Settings Catalog resources. @@ -135,7 +142,6 @@ func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.Creat // are properly read and mapped into the Terraform state, providing a complete view // of the resource's current configuration on the server. func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - tflog.Debug(ctx, fmt.Sprintf("Starting Read method for: %s_%s", r.ProviderTypeName, r.TypeName)) resp.Diagnostics.Append(req.State.Get(ctx, &object)...) @@ -151,19 +157,24 @@ func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadReq } defer cancel() - respResource, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Get(ctx, nil) + err := retry.RetryableIntuneOperation(ctx, "read resource", retry.IntuneRead, func() error { + respResource, err := r.client. + DeviceManagement(). + ConfigurationPolicies(). + ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). + Get(ctx, nil) + if err != nil { + return err + } + MapRemoteResourceStateToTerraform(ctx, &object, respResource) + return nil + }) if err != nil { errors.HandleGraphError(ctx, err, resp, "Read", r.ReadPermissions) return } - MapRemoteResourceStateToTerraform(ctx, &object, respResource) - settingsConfig := graphcustom.GetRequestConfig{ APIVersion: graphcustom.GraphAPIBeta, Endpoint: r.ResourcePath, @@ -175,33 +186,43 @@ func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadReq }, } - respSettings, err := graphcustom.GetRequestByResourceId( - ctx, - r.client.GetAdapter(), - settingsConfig, - ) + err = retry.RetryableIntuneOperation(ctx, "read resource", retry.IntuneRead, func() error { + respSettings, err := graphcustom.GetRequestByResourceId( + ctx, + r.client.GetAdapter(), + settingsConfig, + ) + if err != nil { + return err + } + MapRemoteSettingsStateToTerraform(ctx, &object, respSettings) + return nil + }) if err != nil { - errors.HandleGraphError(ctx, err, resp, "Create - Settings Fetch", r.ReadPermissions) + errors.HandleGraphError(ctx, err, resp, "Read", r.ReadPermissions) return } - MapRemoteSettingsStateToTerraform(ctx, &object, respSettings) - - respAssignments, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Assignments(). - Get(context.Background(), nil) + err = retry.RetryableAssignmentOperation(ctx, "read resource", func() error { + respAssignments, err := r.client. + DeviceManagement(). + ConfigurationPolicies(). + ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). + Assignments(). + Get(context.Background(), nil) + if err != nil { + return err + } + MapRemoteAssignmentStateToTerraform(ctx, &object, respAssignments) + return nil + }) if err != nil { errors.HandleGraphError(ctx, err, resp, "Read", r.ReadPermissions) return } - MapRemoteAssignmentStateToTerraform(ctx, &object, respAssignments) - resp.Diagnostics.Append(resp.State.Set(ctx, &object)...) if resp.Diagnostics.HasError() { return @@ -224,7 +245,6 @@ func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadReq // The function ensures that both the settings and assignments are updated atomically, // and the final state reflects the actual state of the resource on the server. func (r *SettingsCatalogResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - tflog.Debug(ctx, fmt.Sprintf("Starting Update of resource: %s_%s", r.ProviderTypeName, r.TypeName)) resp.Diagnostics.Append(req.Plan.Get(ctx, &object)...) @@ -254,7 +274,11 @@ func (r *SettingsCatalogResource) Update(ctx context.Context, req resource.Updat RequestBody: requestBody, } - err = graphcustom.PutRequestByResourceId(ctx, r.client.GetAdapter(), putRequest) + // Use retryableOperation for main resource update + err = retry.RetryableIntuneOperation(ctx, "update resource", retry.IntuneWrite, func() error { + return graphcustom.PutRequestByResourceId(ctx, r.client.GetAdapter(), putRequest) + }) + if err != nil { errors.HandleGraphError(ctx, err, resp, "Update", r.ReadPermissions) return @@ -269,12 +293,16 @@ func (r *SettingsCatalogResource) Update(ctx context.Context, req resource.Updat return } - _, err = r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Assign(). - Post(ctx, requestAssignment, nil) + // Use retryableAssignmentOperation for assignment update + err = retry.RetryableAssignmentOperation(ctx, "update assignment", func() error { + _, err := r.client. + DeviceManagement(). + ConfigurationPolicies(). + ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). + Assign(). + Post(ctx, requestAssignment, nil) + return err + }) if err != nil { errors.HandleGraphError(ctx, err, resp, "Update", r.WritePermissions) @@ -313,7 +341,6 @@ func (r *SettingsCatalogResource) Update(ctx context.Context, req resource.Updat // // All assignments and settings associated with the resource are automatically removed as part of the deletion. func (r *SettingsCatalogResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - tflog.Debug(ctx, fmt.Sprintf("Starting deletion of resource: %s_%s", r.ProviderTypeName, r.TypeName)) resp.Diagnostics.Append(req.State.Get(ctx, &object)...) @@ -327,11 +354,13 @@ func (r *SettingsCatalogResource) Delete(ctx context.Context, req resource.Delet } defer cancel() - err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Delete(ctx, nil) + err := retry.RetryableIntuneOperation(ctx, "delete resource", retry.IntuneWrite, func() error { + return r.client. + DeviceManagement(). + ConfigurationPolicies(). + ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). + Delete(ctx, nil) + }) if err != nil { errors.HandleGraphError(ctx, err, resp, "Delete", r.ReadPermissions) diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go.back b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go.back new file mode 100644 index 00000000..3ab86cb8 --- /dev/null +++ b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go.back @@ -0,0 +1,344 @@ +package graphBetaSettingsCatalog + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/client/graphcustom" + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/crud" + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/errors" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var ( + // mutex needed to lock Create requests during parallel runs to avoid overwhelming api and resulting in stating issues + mu sync.Mutex + + // object is the resource model for the Endpoint Privilege Management resource + object SettingsCatalogProfileResourceModel +) + +// Create handles the Create operation for Settings Catalog resources. +// +// - Retrieves the planned configuration from the create request +// - Constructs the resource request body from the plan +// - Sends POST request to create the base resource and settings +// - Captures the new resource ID from the response +// - Constructs and sends assignment configuration if specified +// - Sets initial state with planned values +// - Calls Read operation to fetch the latest state from the API +// - Updates the final state with the fresh data from the API +// +// The function ensures that both the settings catalog profile and its assignments +// (if specified) are created properly. The settings must be defined during creation +// as they are required for a successful deployment, while assignments are optional. +func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + + mu.Lock() + defer mu.Unlock() + + tflog.Debug(ctx, fmt.Sprintf("Starting creation of resource: %s_%s", r.ProviderTypeName, r.TypeName)) + + resp.Diagnostics.Append(req.Plan.Get(ctx, &object)...) + if resp.Diagnostics.HasError() { + return + } + + ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + if cancel == nil { + return + } + defer cancel() + + requestBody, err := constructResource(ctx, &object) + if err != nil { + resp.Diagnostics.AddError( + "Error constructing resource for Create method", + fmt.Sprintf("Could not construct resource: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()), + ) + return + } + + requestResource, err := r.client. + DeviceManagement(). + ConfigurationPolicies(). + Post(context.Background(), requestBody, nil) + + if err != nil { + errors.HandleGraphError(ctx, err, resp, "Create", r.WritePermissions) + return + } + + object.ID = types.StringValue(*requestResource.GetId()) + + if object.Assignments != nil { + requestAssignment, err := constructAssignment(ctx, &object) + if err != nil { + resp.Diagnostics.AddError( + "Error constructing assignment for create method", + fmt.Sprintf("Could not construct assignment: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()), + ) + return + } + + _, err = r.client. + DeviceManagement(). + ConfigurationPolicies(). + ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). + Assign(). + Post(ctx, requestAssignment, nil) + + if err != nil { + errors.HandleGraphError(ctx, err, resp, "Create", r.WritePermissions) + return + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &object)...) + if resp.Diagnostics.HasError() { + return + } + + readResp := &resource.ReadResponse{ + State: resp.State, + } + r.Read(ctx, resource.ReadRequest{ + State: resp.State, + ProviderMeta: req.ProviderMeta, + }, readResp) + + resp.Diagnostics.Append(readResp.Diagnostics...) + if resp.Diagnostics.HasError() { + return + } + + resp.State = readResp.State + + tflog.Debug(ctx, fmt.Sprintf("Finished Update Method: %s_%s", r.ProviderTypeName, r.TypeName)) +} + +// Read handles the Read operation for Settings Catalog resources. +// +// - Retrieves the current state from the read request +// - Gets the base resource details from the API +// - Maps the base resource details to Terraform state +// - Gets the settings configuration from the API +// - Maps the settings configuration to Terraform state +// - Gets the assignments configuration from the API +// - Maps the assignments configuration to Terraform state +// +// The function ensures that all components (base resource, settings, and assignments) +// are properly read and mapped into the Terraform state, providing a complete view +// of the resource's current configuration on the server. +func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + + tflog.Debug(ctx, fmt.Sprintf("Starting Read method for: %s_%s", r.ProviderTypeName, r.TypeName)) + + resp.Diagnostics.Append(req.State.Get(ctx, &object)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, object.ID.ValueString())) + + ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + if cancel == nil { + return + } + defer cancel() + + respResource, err := r.client. + DeviceManagement(). + ConfigurationPolicies(). + ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). + Get(ctx, nil) + + if err != nil { + errors.HandleGraphError(ctx, err, resp, "Read", r.ReadPermissions) + return + } + + MapRemoteResourceStateToTerraform(ctx, &object, respResource) + + settingsConfig := graphcustom.GetRequestConfig{ + APIVersion: graphcustom.GraphAPIBeta, + Endpoint: r.ResourcePath, + EndpointSuffix: "/settings", + ResourceIDPattern: "('id')", + ResourceID: object.ID.ValueString(), + QueryParameters: map[string]string{ + "$expand": "children", + }, + } + + respSettings, err := graphcustom.GetRequestByResourceId( + ctx, + r.client.GetAdapter(), + settingsConfig, + ) + + if err != nil { + errors.HandleGraphError(ctx, err, resp, "Create - Settings Fetch", r.ReadPermissions) + return + } + + MapRemoteSettingsStateToTerraform(ctx, &object, respSettings) + + respAssignments, err := r.client. + DeviceManagement(). + ConfigurationPolicies(). + ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). + Assignments(). + Get(context.Background(), nil) + + if err != nil { + errors.HandleGraphError(ctx, err, resp, "Read", r.ReadPermissions) + return + } + + MapRemoteAssignmentStateToTerraform(ctx, &object, respAssignments) + + resp.Diagnostics.Append(resp.State.Set(ctx, &object)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("Finished Read Method: %s_%s", r.ProviderTypeName, r.TypeName)) +} + +// Update handles the Update operation for Settings Catalog resources. +// +// - Retrieves the planned changes from the update request +// - Constructs the resource request body from the plan +// - Sends PUT request to update the base resource and settings +// - Constructs the assignment request body from the plan +// - Sends POST request to update the assignments +// - Sets initial state with planned values +// - Calls Read operation to fetch the latest state from the API +// - Updates the final state with the fresh data from the API +// +// The function ensures that both the settings and assignments are updated atomically, +// and the final state reflects the actual state of the resource on the server. +func (r *SettingsCatalogResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + + tflog.Debug(ctx, fmt.Sprintf("Starting Update of resource: %s_%s", r.ProviderTypeName, r.TypeName)) + + resp.Diagnostics.Append(req.Plan.Get(ctx, &object)...) + if resp.Diagnostics.HasError() { + return + } + + ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + if cancel == nil { + return + } + defer cancel() + + requestBody, err := constructResource(ctx, &object) + if err != nil { + resp.Diagnostics.AddError( + "Error constructing resource for Update method", + fmt.Sprintf("Could not construct resource: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()), + ) + return + } + + putRequest := graphcustom.PutRequestConfig{ + APIVersion: graphcustom.GraphAPIBeta, + Endpoint: r.ResourcePath, + ResourceID: object.ID.ValueString(), + RequestBody: requestBody, + } + + err = graphcustom.PutRequestByResourceId(ctx, r.client.GetAdapter(), putRequest) + if err != nil { + errors.HandleGraphError(ctx, err, resp, "Update", r.ReadPermissions) + return + } + + requestAssignment, err := constructAssignment(ctx, &object) + if err != nil { + resp.Diagnostics.AddError( + "Error constructing assignment for update method", + fmt.Sprintf("Could not construct assignment: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()), + ) + return + } + + _, err = r.client. + DeviceManagement(). + ConfigurationPolicies(). + ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). + Assign(). + Post(ctx, requestAssignment, nil) + + if err != nil { + errors.HandleGraphError(ctx, err, resp, "Update", r.WritePermissions) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &object)...) + if resp.Diagnostics.HasError() { + return + } + + readResp := &resource.ReadResponse{ + State: resp.State, + } + r.Read(ctx, resource.ReadRequest{ + State: resp.State, + ProviderMeta: req.ProviderMeta, + }, readResp) + + resp.Diagnostics.Append(readResp.Diagnostics...) + if resp.Diagnostics.HasError() { + return + } + + resp.State = readResp.State + + tflog.Debug(ctx, fmt.Sprintf("Finished Update Method: %s_%s", r.ProviderTypeName, r.TypeName)) +} + +// Delete handles the Delete operation for Settings Catalog resources. +// +// - Retrieves the current state from the delete request +// - Validates the state data and timeout configuration +// - Sends DELETE request to remove the resource from the API +// - Cleans up by removing the resource from Terraform state +// +// All assignments and settings associated with the resource are automatically removed as part of the deletion. +func (r *SettingsCatalogResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + + tflog.Debug(ctx, fmt.Sprintf("Starting deletion of resource: %s_%s", r.ProviderTypeName, r.TypeName)) + + resp.Diagnostics.Append(req.State.Get(ctx, &object)...) + if resp.Diagnostics.HasError() { + return + } + + ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + if cancel == nil { + return + } + defer cancel() + + err := r.client. + DeviceManagement(). + ConfigurationPolicies(). + ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). + Delete(ctx, nil) + + if err != nil { + errors.HandleGraphError(ctx, err, resp, "Delete", r.ReadPermissions) + return + } + + tflog.Debug(ctx, fmt.Sprintf("Finished Delete Method: %s_%s", r.ProviderTypeName, r.TypeName)) + + resp.State.RemoveResource(ctx) +} diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/model.go b/internal/resources/device_and_app_management/beta/settings_catalog/model.go index 756909a5..49b959c5 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/model.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/model.go @@ -10,19 +10,19 @@ import ( // SettingsCatalogProfileResourceModel holds the configuration for a Settings Catalog profile. // Reference: https://learn.microsoft.com/en-us/graph/api/resources/intune-deviceconfigv2-devicemanagementconfigurationpolicy?view=graph-rest-beta type SettingsCatalogProfileResourceModel struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - Platforms types.String `tfsdk:"platforms"` - Technologies []types.String `tfsdk:"technologies"` - SettingsCount types.Int64 `tfsdk:"settings_count"` - RoleScopeTagIds []types.String `tfsdk:"role_scope_tag_ids"` - LastModifiedDateTime types.String `tfsdk:"last_modified_date_time"` - CreatedDateTime types.String `tfsdk:"created_date_time"` - Settings types.String `tfsdk:"settings"` - IsAssigned types.Bool `tfsdk:"is_assigned"` - Assignments *sharedmodels.SettingsCatalogSettingsAssignmentResourceModel `tfsdk:"assignments"` - Timeouts timeouts.Value `tfsdk:"timeouts"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Platforms types.String `tfsdk:"platforms"` + Technologies []types.String `tfsdk:"technologies"` + RoleScopeTagIds []types.String `tfsdk:"role_scope_tag_ids"` + //SettingsCount types.Int64 `tfsdk:"settings_count"` + //IsAssigned types.Bool `tfsdk:"is_assigned"` + //LastModifiedDateTime types.String `tfsdk:"last_modified_date_time"` + //CreatedDateTime types.String `tfsdk:"created_date_time"` + Settings types.String `tfsdk:"settings"` + Assignments *sharedmodels.SettingsCatalogSettingsAssignmentResourceModel `tfsdk:"assignments"` + Timeouts timeouts.Value `tfsdk:"timeouts"` } // DeviceConfigV2GraphServiceModel is a struct that represents the JSON structure of settings catalog settings @@ -244,246 +244,3 @@ var DeviceConfigV2GraphServiceModel struct { } `json:"settingInstance"` } `json:"settingsDetails"` } - -// DeviceConfigV2GraphServiceModel is a struct that represents the JSON structure of settings catalog settings. -// This struct is used to unmarshal the settings JSON string into a structured format. -// It represents windows, macOS, and iOS settings settings catalog settings. -// var DeviceConfigV2GraphServiceModel struct { -// SettingsDetails []struct { -// ID string `json:"id"` -// SettingInstance struct { -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` - -// // For choice settings -// ChoiceSettingValue *struct { -// Children []struct { -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` -// SettingInstanceTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingInstanceTemplateReference"` - -// // For SimpleSettingCollectionValue within Choice children -// SimpleSettingCollectionValue []struct { -// ODataType string `json:"@odata.type"` -// Value string `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"simpleSettingCollectionValue,omitempty"` - -// // For GroupSettingCollectionValue within Choice children -// GroupSettingCollectionValue []struct { -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// Children []struct { -// SimpleSettingValue *struct { -// ODataType string `json:"@odata.type"` -// Value interface{} `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"simpleSettingValue,omitempty"` -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` -// SettingInstanceTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingInstanceTemplateReference"` -// } `json:"children"` -// } `json:"groupSettingCollectionValue,omitempty"` - -// // For simple settings within choice children -// SimpleSettingValue *struct { -// ODataType string `json:"@odata.type"` -// Value interface{} `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"simpleSettingValue,omitempty"` - -// // For nested choice settings within choice children -// ChoiceSettingValue *struct { -// Value string `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// Children []struct { -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` -// } `json:"children"` -// } `json:"choiceSettingValue,omitempty"` -// } `json:"children"` - -// Value string `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"choiceSettingValue,omitempty"` - -// // For choice setting collections -// ChoiceSettingCollectionValue []struct { -// Children []struct { -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` - -// // For nested simple settings within choice setting collection -// SimpleSettingValue *struct { -// ODataType string `json:"@odata.type"` -// Value interface{} `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"simpleSettingValue,omitempty"` - -// // For nested simple setting collection within choice setting collection -// SimpleSettingCollectionValue []struct { -// ODataType string `json:"@odata.type"` -// Value string `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"simpleSettingCollectionValue,omitempty"` - -// SettingInstanceTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingInstanceTemplateReference"` -// } `json:"children"` - -// Value string `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"choiceSettingCollectionValue,omitempty"` - -// // For group setting collections (Level 1) -// GroupSettingCollectionValue []struct { -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// Children []struct { -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` - -// // For nested group setting collections within group setting collection (Level 2) -// GroupSettingCollectionValue []struct { -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// Children []struct { -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` -// SettingInstanceTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingInstanceTemplateReference"` - -// // For nested group setting collections within group setting collection within group setting collection (Level 3) -// GroupSettingCollectionValue []struct { -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// Children []struct { -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` -// SettingInstanceTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingInstanceTemplateReference"` - -// // For nested choice settings within group setting collection within group setting collection within group setting collection (Level 4) -// ChoiceSettingValue *struct { -// Value string `json:"value"` -// Children []struct { -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` -// SettingInstanceTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingInstanceTemplateReference,omitempty"` - -// // For nested simple settings within choice settings within group setting collection within group setting collection within group setting collection (Level 5) -// SimpleSettingValue *struct { -// ODataType string `json:"@odata.type"` -// Value interface{} `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"simpleSettingValue,omitempty"` -// } `json:"children"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"choiceSettingValue,omitempty"` - -// // For simple settings within group setting collection within group setting collection within group setting collection (Level 4) -// SimpleSettingValue *struct { -// ODataType string `json:"@odata.type"` -// Value interface{} `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"simpleSettingValue,omitempty"` - -// // For simple settings collection within group setting collection within group setting collection within group setting collection (Level 4) -// SimpleSettingCollectionValue []struct { -// ODataType string `json:"@odata.type"` -// Value string `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"simpleSettingCollectionValue,omitempty"` -// } `json:"children"` -// } `json:"groupSettingCollectionValue,omitempty"` - -// // For nested simple settings within group setting collection within group setting collection (Level 3) -// SimpleSettingValue *struct { -// ODataType string `json:"@odata.type"` -// Value interface{} `json:"value"` -// ValueState string `json:"valueState,omitempty"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"simpleSettingValue,omitempty"` - -// // For nested simple setting collections within group setting collection within group setting collection (Level 3) -// SimpleSettingCollectionValue []struct { -// ODataType string `json:"@odata.type"` -// Value string `json:"value"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"simpleSettingCollectionValue,omitempty"` - -// // For nested choice settings within group setting collection within group setting collection (Level 3) -// ChoiceSettingValue *struct { -// Value string `json:"value"` -// Children []struct { -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` -// // For nested simple setting within choice settings within group setting collection within group setting collection (Level 4) -// SimpleSettingValue *struct { -// ODataType string `json:"@odata.type"` -// Value interface{} `json:"value"` -// ValueState string `json:"valueState,omitempty"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"simpleSettingValue,omitempty"` -// } `json:"children"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"choiceSettingValue,omitempty"` -// } `json:"children"` -// } `json:"groupSettingCollectionValue,omitempty"` - -// // For nested simple settings (string, integer, secret) within group setting collection (Level 2) -// SimpleSettingValue *struct { -// ODataType string `json:"@odata.type"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// Value interface{} `json:"value"` -// ValueState string `json:"valueState,omitempty"` -// } `json:"simpleSettingValue,omitempty"` - -// // For nested choice settings within group setting collection (Level 2) -// ChoiceSettingValue *struct { -// Value string `json:"value"` -// Children []struct { -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` -// SettingInstanceTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingInstanceTemplateReference"` - -// SimpleSettingValue *struct { -// ODataType string `json:"@odata.type"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// Value interface{} `json:"value"` -// } `json:"simpleSettingValue,omitempty"` - -// ChoiceSettingValue *struct { -// Children []struct { -// ODataType string `json:"@odata.type"` -// SettingDefinitionId string `json:"settingDefinitionId"` -// } `json:"children"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// Value string `json:"value"` -// } `json:"choiceSettingValue,omitempty"` -// } `json:"children"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// } `json:"choiceSettingValue,omitempty"` - -// // For nested simple setting collections within group setting collection (Level 2) -// SimpleSettingCollectionValue []struct { -// ODataType string `json:"@odata.type"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// Value string `json:"value"` -// } `json:"simpleSettingCollectionValue,omitempty"` - -// SettingInstanceTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingInstanceTemplateReference"` -// } `json:"children"` -// } `json:"groupSettingCollectionValue,omitempty"` - -// // For simple settings -// SimpleSettingValue *struct { -// ODataType string `json:"@odata.type"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// Value interface{} `json:"value"` -// } `json:"simpleSettingValue,omitempty"` - -// // For simple collection settings -// SimpleSettingCollectionValue []struct { -// ODataType string `json:"@odata.type"` -// SettingValueTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingValueTemplateReference"` -// Value string `json:"value"` -// } `json:"simpleSettingCollectionValue,omitempty"` - -// SettingInstanceTemplateReference graphmodels.DeviceManagementConfigurationSettingValueTemplateReferenceable `json:"settingInstanceTemplateReference"` -// } `json:"settingInstance"` -// } `json:"settingsDetails"` -// } diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/resource_v7.go b/internal/resources/device_and_app_management/beta/settings_catalog/resource_v7.go index 2f98ed9e..63c2cb06 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/resource_v7.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/resource_v7.go @@ -12,7 +12,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" msgraphbetasdk "github.com/microsoftgraph/msgraph-beta-sdk-go" @@ -166,28 +165,28 @@ func (r *SettingsCatalogResource) Schema(ctx context.Context, req resource.Schem }, }, - "created_date_time": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - MarkdownDescription: "Creation date and time of the settings catalog policy", - }, - "last_modified_date_time": schema.StringAttribute{ - Computed: true, - MarkdownDescription: "Last modification date and time of the settings catalog policy", - }, - "settings_count": schema.Int64Attribute{ - Computed: true, - MarkdownDescription: "Number of settings catalog settings with the policy. This will change over time as the resource is updated.", - }, - "is_assigned": schema.BoolAttribute{ - Computed: true, - PlanModifiers: []planmodifier.Bool{ - planmodifiers.UseStateForUnknownBool(), - }, - MarkdownDescription: "Indicates if the policy is assigned to any scope", - }, + // "created_date_time": schema.StringAttribute{ + // Computed: true, + // PlanModifiers: []planmodifier.String{ + // stringplanmodifier.UseStateForUnknown(), + // }, + // MarkdownDescription: "Creation date and time of the settings catalog policy", + // }, + // "last_modified_date_time": schema.StringAttribute{ + // Computed: true, + // MarkdownDescription: "Last modification date and time of the settings catalog policy", + // }, + // "settings_count": schema.Int64Attribute{ + // Computed: true, + // MarkdownDescription: "Number of settings catalog settings with the policy. This will change over time as the resource is updated.", + // }, + // "is_assigned": schema.BoolAttribute{ + // Computed: true, + // PlanModifiers: []planmodifier.Bool{ + // planmodifiers.UseStateForUnknownBool(), + // }, + // MarkdownDescription: "Indicates if the policy is assigned to any scope", + // }, "assignments": commonschema.SettingsCatalogAssignmentsSchema(), "timeouts": commonschema.Timeouts(ctx), }, diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/state_base_resource.go b/internal/resources/device_and_app_management/beta/settings_catalog/state_base_resource.go index 61331bde..e0b13a6b 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/state_base_resource.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/state_base_resource.go @@ -23,11 +23,11 @@ func MapRemoteResourceStateToTerraform(ctx context.Context, data *SettingsCatalo data.ID = types.StringValue(state.StringPtrToString(remoteResource.GetId())) data.Name = types.StringValue(state.StringPtrToString(remoteResource.GetName())) data.Description = types.StringValue(state.StringPtrToString(remoteResource.GetDescription())) - data.CreatedDateTime = state.TimeToString(remoteResource.GetCreatedDateTime()) - data.LastModifiedDateTime = state.TimeToString(remoteResource.GetLastModifiedDateTime()) - data.SettingsCount = state.Int32PtrToTypeInt64(remoteResource.GetSettingCount()) data.RoleScopeTagIds = state.SliceToTypeStringSlice(remoteResource.GetRoleScopeTagIds()) - data.IsAssigned = state.BoolPtrToTypeBool(remoteResource.GetIsAssigned()) + // data.IsAssigned = state.BoolPtrToTypeBool(remoteResource.GetIsAssigned()) + // data.CreatedDateTime = state.TimeToString(remoteResource.GetCreatedDateTime()) + // data.LastModifiedDateTime = state.TimeToString(remoteResource.GetLastModifiedDateTime()) + // data.SettingsCount = state.Int32PtrToTypeInt64(remoteResource.GetSettingCount()) if platforms := remoteResource.GetPlatforms(); platforms != nil { data.Platforms = state.EnumPtrToTypeString(platforms) From d3327a313a16e54cf7bb79e2ec80a077fac2dc3f Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 16:47:43 +0000 Subject: [PATCH 09/13] Add timeout constants for resource operations in all resources --- .../v1.0/cloud_pc_device_image/read.go | 2 +- internal/resources/_resource_template/crud.go | 8 +-- .../common/retry/retry_assignments.go | 2 +- .../beta/assignment_filter/crud.go | 8 +-- .../beta/assignment_filter/resource.go | 6 ++- .../beta/browser_site/crud.go | 8 +-- .../beta/browser_site/resource.go | 6 ++- .../beta/browser_site_list/crud.go | 8 +-- .../beta/browser_site_list/resource.go | 6 ++- .../endpoint_privilege_management/resource.go | 6 ++- .../beta/linux_platform_script/resource.go | 6 ++- .../m365_apps_installation_options/crud.go | 6 +-- .../resource.go | 6 ++- .../beta/macos_pkg_app/crud.go | 8 +-- .../beta/macos_pkg_app/resource.go | 6 ++- .../beta/macos_platform_script/resource.go | 6 ++- .../beta/mobile_app_assignment/crud.go | 8 +-- .../beta/mobile_app_assignment/resource.go | 6 ++- .../beta/role_definition/crud.go | 8 +-- .../beta/role_definition/resource.go | 6 ++- .../beta/settings_catalog/crud.go | 30 +++++------ .../beta/settings_catalog/model.go | 26 +++++----- .../beta/settings_catalog/resource_v7.go | 51 ++++++++++--------- .../settings_catalog/state_base_resource.go | 8 +-- .../beta/win32_lob_app/resource.go | 6 ++- .../beta/windows_platform_script/resource.go | 6 ++- .../beta/winget_app/crud.go | 8 +-- .../beta/winget_app/resource.go | 6 ++- .../v1.0/cloud_pc_device_image/crud.go | 8 +-- .../v1.0/cloud_pc_device_image/resource.go | 6 ++- .../v1.0/cloud_pc_provisioning_policy/crud.go | 8 +-- .../cloud_pc_provisioning_policy/resource.go | 6 ++- .../v1.0/cloud_pc_user_setting/crud.go | 8 +-- .../v1.0/cloud_pc_user_setting/resource.go | 6 ++- .../v1.0/role_definition/crud.go | 8 +-- .../v1.0/role_definition/resource.go | 6 ++- .../beta/conditional_access_policy/crud.go | 8 +-- .../conditional_access_policy/resource.go | 6 ++- 38 files changed, 205 insertions(+), 132 deletions(-) diff --git a/internal/datasources/device_and_app_management/v1.0/cloud_pc_device_image/read.go b/internal/datasources/device_and_app_management/v1.0/cloud_pc_device_image/read.go index 80214ff2..5cf9153f 100644 --- a/internal/datasources/device_and_app_management/v1.0/cloud_pc_device_image/read.go +++ b/internal/datasources/device_and_app_management/v1.0/cloud_pc_device_image/read.go @@ -19,7 +19,7 @@ func (d *CloudPcDeviceImageDataSource) Read(ctx context.Context, req datasource. return } - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/_resource_template/crud.go b/internal/resources/_resource_template/crud.go index c5ee6ade..19657ef0 100644 --- a/internal/resources/_resource_template/crud.go +++ b/internal/resources/_resource_template/crud.go @@ -30,7 +30,7 @@ func (r *ResourceTemplateResource) Create(ctx context.Context, req resource.Crea return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -80,7 +80,7 @@ func (r *ResourceTemplateResource) Read(ctx context.Context, req resource.ReadRe tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -118,7 +118,7 @@ func (r *ResourceTemplateResource) Update(ctx context.Context, req resource.Upda return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -163,7 +163,7 @@ func (r *ResourceTemplateResource) Delete(ctx context.Context, req resource.Dele return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/common/retry/retry_assignments.go b/internal/resources/common/retry/retry_assignments.go index 9920aba3..3eb20459 100644 --- a/internal/resources/common/retry/retry_assignments.go +++ b/internal/resources/common/retry/retry_assignments.go @@ -20,7 +20,7 @@ func RetryableAssignmentOperation(ctx context.Context, operation string, fn func tenSecondLimit = 500 // requests per 10 seconds per app per tenant hourlyLimit = 15000 // requests per hour per app per tenant maxBackoff = 10 * time.Second - baseDelay = 3 * time.Second // Higher base delay for assignments + baseDelay = 3 * time.Second ) for { diff --git a/internal/resources/device_and_app_management/beta/assignment_filter/crud.go b/internal/resources/device_and_app_management/beta/assignment_filter/crud.go index 0f3308db..f6428bfc 100644 --- a/internal/resources/device_and_app_management/beta/assignment_filter/crud.go +++ b/internal/resources/device_and_app_management/beta/assignment_filter/crud.go @@ -23,7 +23,7 @@ func (r *AssignmentFilterResource) Create(ctx context.Context, req resource.Crea return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -73,7 +73,7 @@ func (r *AssignmentFilterResource) Read(ctx context.Context, req resource.ReadRe tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -111,7 +111,7 @@ func (r *AssignmentFilterResource) Update(ctx context.Context, req resource.Upda return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -156,7 +156,7 @@ func (r *AssignmentFilterResource) Delete(ctx context.Context, req resource.Dele return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/beta/assignment_filter/resource.go b/internal/resources/device_and_app_management/beta/assignment_filter/resource.go index 3c5aeb0d..92b4e487 100644 --- a/internal/resources/device_and_app_management/beta/assignment_filter/resource.go +++ b/internal/resources/device_and_app_management/beta/assignment_filter/resource.go @@ -18,7 +18,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_assignment_filter" + ResourceName = "graph_beta_device_and_app_management_assignment_filter" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/beta/browser_site/crud.go b/internal/resources/device_and_app_management/beta/browser_site/crud.go index 8a483eb7..cb44dee3 100644 --- a/internal/resources/device_and_app_management/beta/browser_site/crud.go +++ b/internal/resources/device_and_app_management/beta/browser_site/crud.go @@ -23,7 +23,7 @@ func (r *BrowserSiteResource) Create(ctx context.Context, req resource.CreateReq return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -84,7 +84,7 @@ func (r *BrowserSiteResource) Read(ctx context.Context, req resource.ReadRequest tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -128,7 +128,7 @@ func (r *BrowserSiteResource) Update(ctx context.Context, req resource.UpdateReq return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -179,7 +179,7 @@ func (r *BrowserSiteResource) Delete(ctx context.Context, req resource.DeleteReq return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/beta/browser_site/resource.go b/internal/resources/device_and_app_management/beta/browser_site/resource.go index 7d23dba5..adc876f1 100644 --- a/internal/resources/device_and_app_management/beta/browser_site/resource.go +++ b/internal/resources/device_and_app_management/beta/browser_site/resource.go @@ -15,7 +15,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_browser_site" + ResourceName = "graph_beta_device_and_app_management_browser_site" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/beta/browser_site_list/crud.go b/internal/resources/device_and_app_management/beta/browser_site_list/crud.go index 8c926d4c..b10320c0 100644 --- a/internal/resources/device_and_app_management/beta/browser_site_list/crud.go +++ b/internal/resources/device_and_app_management/beta/browser_site_list/crud.go @@ -23,7 +23,7 @@ func (r *BrowserSiteListResource) Create(ctx context.Context, req resource.Creat return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -74,7 +74,7 @@ func (r *BrowserSiteListResource) Read(ctx context.Context, req resource.ReadReq tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -114,7 +114,7 @@ func (r *BrowserSiteListResource) Update(ctx context.Context, req resource.Updat return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -161,7 +161,7 @@ func (r *BrowserSiteListResource) Delete(ctx context.Context, req resource.Delet return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/beta/browser_site_list/resource.go b/internal/resources/device_and_app_management/beta/browser_site_list/resource.go index 5d49a7d6..1616d3d4 100644 --- a/internal/resources/device_and_app_management/beta/browser_site_list/resource.go +++ b/internal/resources/device_and_app_management/beta/browser_site_list/resource.go @@ -17,7 +17,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_browser_site_list" + ResourceName = "graph_beta_device_and_app_management_browser_site_list" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/beta/endpoint_privilege_management/resource.go b/internal/resources/device_and_app_management/beta/endpoint_privilege_management/resource.go index 64e09946..000c717d 100644 --- a/internal/resources/device_and_app_management/beta/endpoint_privilege_management/resource.go +++ b/internal/resources/device_and_app_management/beta/endpoint_privilege_management/resource.go @@ -12,7 +12,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_privilege_management_elevations" + ResourceName = "graph_beta_device_and_app_management_privilege_management_elevations" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/beta/linux_platform_script/resource.go b/internal/resources/device_and_app_management/beta/linux_platform_script/resource.go index 7abaaf91..3b2112fc 100644 --- a/internal/resources/device_and_app_management/beta/linux_platform_script/resource.go +++ b/internal/resources/device_and_app_management/beta/linux_platform_script/resource.go @@ -13,7 +13,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_linux_platform_script" + ResourceName = "graph_beta_device_and_app_management_linux_platform_script" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) // var ( diff --git a/internal/resources/device_and_app_management/beta/m365_apps_installation_options/crud.go b/internal/resources/device_and_app_management/beta/m365_apps_installation_options/crud.go index 4add3748..d19cd4c0 100644 --- a/internal/resources/device_and_app_management/beta/m365_apps_installation_options/crud.go +++ b/internal/resources/device_and_app_management/beta/m365_apps_installation_options/crud.go @@ -23,7 +23,7 @@ func (r *M365AppsInstallationOptionsResource) Create(ctx context.Context, req re return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -74,7 +74,7 @@ func (r *M365AppsInstallationOptionsResource) Read(ctx context.Context, req reso tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -112,7 +112,7 @@ func (r *M365AppsInstallationOptionsResource) Update(ctx context.Context, req re return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/beta/m365_apps_installation_options/resource.go b/internal/resources/device_and_app_management/beta/m365_apps_installation_options/resource.go index 6d0940ff..28a42c7f 100644 --- a/internal/resources/device_and_app_management/beta/m365_apps_installation_options/resource.go +++ b/internal/resources/device_and_app_management/beta/m365_apps_installation_options/resource.go @@ -15,7 +15,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_m365_apps_installation_options" + ResourceName = "graph_beta_device_and_app_management_m365_apps_installation_options" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/beta/macos_pkg_app/crud.go b/internal/resources/device_and_app_management/beta/macos_pkg_app/crud.go index bf60306a..4f9f83eb 100644 --- a/internal/resources/device_and_app_management/beta/macos_pkg_app/crud.go +++ b/internal/resources/device_and_app_management/beta/macos_pkg_app/crud.go @@ -24,7 +24,7 @@ func (r *MacOSPkgAppResource) Create(ctx context.Context, req resource.CreateReq return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -82,7 +82,7 @@ func (r *MacOSPkgAppResource) Read(ctx context.Context, req resource.ReadRequest tflog.Debug(ctx, fmt.Sprintf("Reading macOS PKG app with ID: %s", state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -128,7 +128,7 @@ func (r *MacOSPkgAppResource) Update(ctx context.Context, req resource.UpdateReq return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -184,7 +184,7 @@ func (r *MacOSPkgAppResource) Delete(ctx context.Context, req resource.DeleteReq return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/beta/macos_pkg_app/resource.go b/internal/resources/device_and_app_management/beta/macos_pkg_app/resource.go index 2a20b24b..00e6dcdf 100644 --- a/internal/resources/device_and_app_management/beta/macos_pkg_app/resource.go +++ b/internal/resources/device_and_app_management/beta/macos_pkg_app/resource.go @@ -13,7 +13,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_macos_pkg_app" + ResourceName = "graph_beta_device_and_app_management_macos_pkg_app" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/beta/macos_platform_script/resource.go b/internal/resources/device_and_app_management/beta/macos_platform_script/resource.go index bce04aa9..f8d95f1e 100644 --- a/internal/resources/device_and_app_management/beta/macos_platform_script/resource.go +++ b/internal/resources/device_and_app_management/beta/macos_platform_script/resource.go @@ -16,7 +16,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_macos_platform_script" + ResourceName = "graph_beta_device_and_app_management_macos_platform_script" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/beta/mobile_app_assignment/crud.go b/internal/resources/device_and_app_management/beta/mobile_app_assignment/crud.go index f4292667..f728ae38 100644 --- a/internal/resources/device_and_app_management/beta/mobile_app_assignment/crud.go +++ b/internal/resources/device_and_app_management/beta/mobile_app_assignment/crud.go @@ -25,7 +25,7 @@ func (r *MobileAppAssignmentResource) Create(ctx context.Context, req resource.C return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -78,7 +78,7 @@ func (r *MobileAppAssignmentResource) Read(ctx context.Context, req resource.Rea tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -137,7 +137,7 @@ func (r *MobileAppAssignmentResource) Update(ctx context.Context, req resource.U return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -186,7 +186,7 @@ func (r *MobileAppAssignmentResource) Delete(ctx context.Context, req resource.D return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/beta/mobile_app_assignment/resource.go b/internal/resources/device_and_app_management/beta/mobile_app_assignment/resource.go index e7658dfd..25122774 100644 --- a/internal/resources/device_and_app_management/beta/mobile_app_assignment/resource.go +++ b/internal/resources/device_and_app_management/beta/mobile_app_assignment/resource.go @@ -14,7 +14,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_mobile_app_assignment" + ResourceName = "graph_beta_device_and_app_management_mobile_app_assignment" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/beta/role_definition/crud.go b/internal/resources/device_and_app_management/beta/role_definition/crud.go index ac674291..14ebb556 100644 --- a/internal/resources/device_and_app_management/beta/role_definition/crud.go +++ b/internal/resources/device_and_app_management/beta/role_definition/crud.go @@ -23,7 +23,7 @@ func (r *RoleDefinitionResource) Create(ctx context.Context, req resource.Create return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -72,7 +72,7 @@ func (r *RoleDefinitionResource) Read(ctx context.Context, req resource.ReadRequ tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -110,7 +110,7 @@ func (r *RoleDefinitionResource) Update(ctx context.Context, req resource.Update return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -156,7 +156,7 @@ func (r *RoleDefinitionResource) Delete(ctx context.Context, req resource.Delete return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/beta/role_definition/resource.go b/internal/resources/device_and_app_management/beta/role_definition/resource.go index 1aaf462b..9db44404 100644 --- a/internal/resources/device_and_app_management/beta/role_definition/resource.go +++ b/internal/resources/device_and_app_management/beta/role_definition/resource.go @@ -13,7 +13,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_role_definition" + ResourceName = "graph_beta_device_and_app_management_role_definition" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go index 75e4127e..1254652f 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go @@ -3,7 +3,6 @@ package graphBetaSettingsCatalog import ( "context" "fmt" - "sync" "time" "github.com/deploymenttheory/terraform-provider-microsoft365/internal/client/graphcustom" @@ -15,14 +14,6 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) -var ( - // mutex needed to lock Create requests during parallel runs to avoid overwhelming api and resulting in stating issues - mu sync.Mutex - - // object is the resource model for the Endpoint Privilege Management resource - object SettingsCatalogProfileResourceModel -) - // Create handles the Create operation for Settings Catalog resources. // // - Retrieves the planned configuration from the create request @@ -38,8 +29,7 @@ var ( // (if specified) are created properly. The settings must be defined during creation // as they are required for a successful deployment, while assignments are optional. func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - mu.Lock() - defer mu.Unlock() + var object SettingsCatalogProfileResourceModel tflog.Debug(ctx, fmt.Sprintf("Starting creation of resource: %s_%s", r.ProviderTypeName, r.TypeName)) @@ -48,7 +38,7 @@ func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.Creat return } - ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -68,7 +58,7 @@ func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.Creat requestBody, opErr = r.client. DeviceManagement(). ConfigurationPolicies(). - Post(context.Background(), requestBody, nil) + Post(ctx, requestBody, nil) return opErr }) @@ -142,6 +132,8 @@ func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.Creat // are properly read and mapped into the Terraform state, providing a complete view // of the resource's current configuration on the server. func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var object SettingsCatalogProfileResourceModel + tflog.Debug(ctx, fmt.Sprintf("Starting Read method for: %s_%s", r.ProviderTypeName, r.TypeName)) resp.Diagnostics.Append(req.State.Get(ctx, &object)...) @@ -151,7 +143,7 @@ func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadReq tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, object.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -210,7 +202,7 @@ func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadReq ConfigurationPolicies(). ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). Assignments(). - Get(context.Background(), nil) + Get(ctx, nil) if err != nil { return err } @@ -245,6 +237,8 @@ func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadReq // The function ensures that both the settings and assignments are updated atomically, // and the final state reflects the actual state of the resource on the server. func (r *SettingsCatalogResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var object SettingsCatalogProfileResourceModel + tflog.Debug(ctx, fmt.Sprintf("Starting Update of resource: %s_%s", r.ProviderTypeName, r.TypeName)) resp.Diagnostics.Append(req.Plan.Get(ctx, &object)...) @@ -252,7 +246,7 @@ func (r *SettingsCatalogResource) Update(ctx context.Context, req resource.Updat return } - ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -341,6 +335,8 @@ func (r *SettingsCatalogResource) Update(ctx context.Context, req resource.Updat // // All assignments and settings associated with the resource are automatically removed as part of the deletion. func (r *SettingsCatalogResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var object SettingsCatalogProfileResourceModel + tflog.Debug(ctx, fmt.Sprintf("Starting deletion of resource: %s_%s", r.ProviderTypeName, r.TypeName)) resp.Diagnostics.Append(req.State.Get(ctx, &object)...) @@ -348,7 +344,7 @@ func (r *SettingsCatalogResource) Delete(ctx context.Context, req resource.Delet return } - ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/model.go b/internal/resources/device_and_app_management/beta/settings_catalog/model.go index 49b959c5..b8e5e542 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/model.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/model.go @@ -10,19 +10,19 @@ import ( // SettingsCatalogProfileResourceModel holds the configuration for a Settings Catalog profile. // Reference: https://learn.microsoft.com/en-us/graph/api/resources/intune-deviceconfigv2-devicemanagementconfigurationpolicy?view=graph-rest-beta type SettingsCatalogProfileResourceModel struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - Platforms types.String `tfsdk:"platforms"` - Technologies []types.String `tfsdk:"technologies"` - RoleScopeTagIds []types.String `tfsdk:"role_scope_tag_ids"` - //SettingsCount types.Int64 `tfsdk:"settings_count"` - //IsAssigned types.Bool `tfsdk:"is_assigned"` - //LastModifiedDateTime types.String `tfsdk:"last_modified_date_time"` - //CreatedDateTime types.String `tfsdk:"created_date_time"` - Settings types.String `tfsdk:"settings"` - Assignments *sharedmodels.SettingsCatalogSettingsAssignmentResourceModel `tfsdk:"assignments"` - Timeouts timeouts.Value `tfsdk:"timeouts"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Platforms types.String `tfsdk:"platforms"` + Technologies []types.String `tfsdk:"technologies"` + RoleScopeTagIds []types.String `tfsdk:"role_scope_tag_ids"` + SettingsCount types.Int64 `tfsdk:"settings_count"` + IsAssigned types.Bool `tfsdk:"is_assigned"` + LastModifiedDateTime types.String `tfsdk:"last_modified_date_time"` + CreatedDateTime types.String `tfsdk:"created_date_time"` + Settings types.String `tfsdk:"settings"` + Assignments *sharedmodels.SettingsCatalogSettingsAssignmentResourceModel `tfsdk:"assignments"` + Timeouts timeouts.Value `tfsdk:"timeouts"` } // DeviceConfigV2GraphServiceModel is a struct that represents the JSON structure of settings catalog settings diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/resource_v7.go b/internal/resources/device_and_app_management/beta/settings_catalog/resource_v7.go index 63c2cb06..b1d0ac27 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/resource_v7.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/resource_v7.go @@ -12,13 +12,18 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" msgraphbetasdk "github.com/microsoftgraph/msgraph-beta-sdk-go" ) const ( - ResourceName = "graph_beta_device_and_app_management_settings_catalog" + ResourceName = "graph_beta_device_and_app_management_settings_catalog" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( @@ -165,28 +170,28 @@ func (r *SettingsCatalogResource) Schema(ctx context.Context, req resource.Schem }, }, - // "created_date_time": schema.StringAttribute{ - // Computed: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.UseStateForUnknown(), - // }, - // MarkdownDescription: "Creation date and time of the settings catalog policy", - // }, - // "last_modified_date_time": schema.StringAttribute{ - // Computed: true, - // MarkdownDescription: "Last modification date and time of the settings catalog policy", - // }, - // "settings_count": schema.Int64Attribute{ - // Computed: true, - // MarkdownDescription: "Number of settings catalog settings with the policy. This will change over time as the resource is updated.", - // }, - // "is_assigned": schema.BoolAttribute{ - // Computed: true, - // PlanModifiers: []planmodifier.Bool{ - // planmodifiers.UseStateForUnknownBool(), - // }, - // MarkdownDescription: "Indicates if the policy is assigned to any scope", - // }, + "created_date_time": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + MarkdownDescription: "Creation date and time of the settings catalog policy", + }, + "last_modified_date_time": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Last modification date and time of the settings catalog policy", + }, + "settings_count": schema.Int64Attribute{ + Computed: true, + MarkdownDescription: "Number of settings catalog settings with the policy. This will change over time as the resource is updated.", + }, + "is_assigned": schema.BoolAttribute{ + Computed: true, + PlanModifiers: []planmodifier.Bool{ + planmodifiers.UseStateForUnknownBool(), + }, + MarkdownDescription: "Indicates if the policy is assigned to any scope", + }, "assignments": commonschema.SettingsCatalogAssignmentsSchema(), "timeouts": commonschema.Timeouts(ctx), }, diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/state_base_resource.go b/internal/resources/device_and_app_management/beta/settings_catalog/state_base_resource.go index e0b13a6b..2e89dfca 100644 --- a/internal/resources/device_and_app_management/beta/settings_catalog/state_base_resource.go +++ b/internal/resources/device_and_app_management/beta/settings_catalog/state_base_resource.go @@ -24,10 +24,10 @@ func MapRemoteResourceStateToTerraform(ctx context.Context, data *SettingsCatalo data.Name = types.StringValue(state.StringPtrToString(remoteResource.GetName())) data.Description = types.StringValue(state.StringPtrToString(remoteResource.GetDescription())) data.RoleScopeTagIds = state.SliceToTypeStringSlice(remoteResource.GetRoleScopeTagIds()) - // data.IsAssigned = state.BoolPtrToTypeBool(remoteResource.GetIsAssigned()) - // data.CreatedDateTime = state.TimeToString(remoteResource.GetCreatedDateTime()) - // data.LastModifiedDateTime = state.TimeToString(remoteResource.GetLastModifiedDateTime()) - // data.SettingsCount = state.Int32PtrToTypeInt64(remoteResource.GetSettingCount()) + data.IsAssigned = state.BoolPtrToTypeBool(remoteResource.GetIsAssigned()) + data.CreatedDateTime = state.TimeToString(remoteResource.GetCreatedDateTime()) + data.LastModifiedDateTime = state.TimeToString(remoteResource.GetLastModifiedDateTime()) + data.SettingsCount = state.Int32PtrToTypeInt64(remoteResource.GetSettingCount()) if platforms := remoteResource.GetPlatforms(); platforms != nil { data.Platforms = state.EnumPtrToTypeString(platforms) diff --git a/internal/resources/device_and_app_management/beta/win32_lob_app/resource.go b/internal/resources/device_and_app_management/beta/win32_lob_app/resource.go index 8e005a99..d6a1d613 100644 --- a/internal/resources/device_and_app_management/beta/win32_lob_app/resource.go +++ b/internal/resources/device_and_app_management/beta/win32_lob_app/resource.go @@ -15,7 +15,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_win32_lob_app" + ResourceName = "graph_beta_device_and_app_management_win32_lob_app" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/beta/windows_platform_script/resource.go b/internal/resources/device_and_app_management/beta/windows_platform_script/resource.go index e304d49c..77d6e844 100644 --- a/internal/resources/device_and_app_management/beta/windows_platform_script/resource.go +++ b/internal/resources/device_and_app_management/beta/windows_platform_script/resource.go @@ -15,7 +15,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_windows_platform_script" + ResourceName = "graph_beta_device_and_app_management_windows_platform_script" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/beta/winget_app/crud.go b/internal/resources/device_and_app_management/beta/winget_app/crud.go index a987d64a..416a463b 100644 --- a/internal/resources/device_and_app_management/beta/winget_app/crud.go +++ b/internal/resources/device_and_app_management/beta/winget_app/crud.go @@ -24,7 +24,7 @@ func (r *WinGetAppResource) Create(ctx context.Context, req resource.CreateReque return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -83,7 +83,7 @@ func (r *WinGetAppResource) Read(ctx context.Context, req resource.ReadRequest, tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -140,7 +140,7 @@ func (r *WinGetAppResource) Update(ctx context.Context, req resource.UpdateReque return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -209,7 +209,7 @@ func (r *WinGetAppResource) Delete(ctx context.Context, req resource.DeleteReque return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/beta/winget_app/resource.go b/internal/resources/device_and_app_management/beta/winget_app/resource.go index 299e7888..bf82c1e5 100644 --- a/internal/resources/device_and_app_management/beta/winget_app/resource.go +++ b/internal/resources/device_and_app_management/beta/winget_app/resource.go @@ -17,7 +17,11 @@ import ( ) const ( - ResourceName = "graph_beta_device_and_app_management_win_get_app" + ResourceName = "graph_beta_device_and_app_management_win_get_app" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/v1.0/cloud_pc_device_image/crud.go b/internal/resources/device_and_app_management/v1.0/cloud_pc_device_image/crud.go index c6abbcf9..92cf85cd 100644 --- a/internal/resources/device_and_app_management/v1.0/cloud_pc_device_image/crud.go +++ b/internal/resources/device_and_app_management/v1.0/cloud_pc_device_image/crud.go @@ -23,7 +23,7 @@ func (r *CloudPcDeviceImageResource) Create(ctx context.Context, req resource.Cr return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -74,7 +74,7 @@ func (r *CloudPcDeviceImageResource) Read(ctx context.Context, req resource.Read tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -113,7 +113,7 @@ func (r *CloudPcDeviceImageResource) Update(ctx context.Context, req resource.Up return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -159,7 +159,7 @@ func (r *CloudPcDeviceImageResource) Delete(ctx context.Context, req resource.De return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/v1.0/cloud_pc_device_image/resource.go b/internal/resources/device_and_app_management/v1.0/cloud_pc_device_image/resource.go index 184d0c27..a317a7bb 100644 --- a/internal/resources/device_and_app_management/v1.0/cloud_pc_device_image/resource.go +++ b/internal/resources/device_and_app_management/v1.0/cloud_pc_device_image/resource.go @@ -16,7 +16,11 @@ import ( ) const ( - ResourceName = "graph_device_and_app_management_cloud_pc_device_image" + ResourceName = "graph_device_and_app_management_cloud_pc_device_image" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/v1.0/cloud_pc_provisioning_policy/crud.go b/internal/resources/device_and_app_management/v1.0/cloud_pc_provisioning_policy/crud.go index 2f6fe3a9..a9b5cc83 100644 --- a/internal/resources/device_and_app_management/v1.0/cloud_pc_provisioning_policy/crud.go +++ b/internal/resources/device_and_app_management/v1.0/cloud_pc_provisioning_policy/crud.go @@ -23,7 +23,7 @@ func (r *CloudPcProvisioningPolicyResource) Create(ctx context.Context, req reso return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -74,7 +74,7 @@ func (r *CloudPcProvisioningPolicyResource) Read(ctx context.Context, req resour tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -113,7 +113,7 @@ func (r *CloudPcProvisioningPolicyResource) Update(ctx context.Context, req reso return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -159,7 +159,7 @@ func (r *CloudPcProvisioningPolicyResource) Delete(ctx context.Context, req reso return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/v1.0/cloud_pc_provisioning_policy/resource.go b/internal/resources/device_and_app_management/v1.0/cloud_pc_provisioning_policy/resource.go index e8a1602c..c10723de 100644 --- a/internal/resources/device_and_app_management/v1.0/cloud_pc_provisioning_policy/resource.go +++ b/internal/resources/device_and_app_management/v1.0/cloud_pc_provisioning_policy/resource.go @@ -16,7 +16,11 @@ import ( ) const ( - ResourceName = "graph_device_and_app_management_cloud_pc_provisioning_policy" + ResourceName = "graph_device_and_app_management_cloud_pc_provisioning_policy" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/v1.0/cloud_pc_user_setting/crud.go b/internal/resources/device_and_app_management/v1.0/cloud_pc_user_setting/crud.go index f8c45b57..0b19998f 100644 --- a/internal/resources/device_and_app_management/v1.0/cloud_pc_user_setting/crud.go +++ b/internal/resources/device_and_app_management/v1.0/cloud_pc_user_setting/crud.go @@ -23,7 +23,7 @@ func (r *CloudPcUserSettingResource) Create(ctx context.Context, req resource.Cr return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -74,7 +74,7 @@ func (r *CloudPcUserSettingResource) Read(ctx context.Context, req resource.Read tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -113,7 +113,7 @@ func (r *CloudPcUserSettingResource) Update(ctx context.Context, req resource.Up return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -159,7 +159,7 @@ func (r *CloudPcUserSettingResource) Delete(ctx context.Context, req resource.De return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/v1.0/cloud_pc_user_setting/resource.go b/internal/resources/device_and_app_management/v1.0/cloud_pc_user_setting/resource.go index 58c25e1e..db2dc3f5 100644 --- a/internal/resources/device_and_app_management/v1.0/cloud_pc_user_setting/resource.go +++ b/internal/resources/device_and_app_management/v1.0/cloud_pc_user_setting/resource.go @@ -15,7 +15,11 @@ import ( ) const ( - ResourceName = "graph_device_and_app_management_cloud_pc_user_setting" + ResourceName = "graph_device_and_app_management_cloud_pc_user_setting" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/device_and_app_management/v1.0/role_definition/crud.go b/internal/resources/device_and_app_management/v1.0/role_definition/crud.go index c6025c6a..149c25f8 100644 --- a/internal/resources/device_and_app_management/v1.0/role_definition/crud.go +++ b/internal/resources/device_and_app_management/v1.0/role_definition/crud.go @@ -23,7 +23,7 @@ func (r *RoleDefinitionResource) Create(ctx context.Context, req resource.Create return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -72,7 +72,7 @@ func (r *RoleDefinitionResource) Read(ctx context.Context, req resource.ReadRequ tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -111,7 +111,7 @@ func (r *RoleDefinitionResource) Update(ctx context.Context, req resource.Update return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -158,7 +158,7 @@ func (r *RoleDefinitionResource) Delete(ctx context.Context, req resource.Delete return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/device_and_app_management/v1.0/role_definition/resource.go b/internal/resources/device_and_app_management/v1.0/role_definition/resource.go index f351c76d..6c45388b 100644 --- a/internal/resources/device_and_app_management/v1.0/role_definition/resource.go +++ b/internal/resources/device_and_app_management/v1.0/role_definition/resource.go @@ -13,7 +13,11 @@ import ( ) const ( - ResourceName = "graph_device_and_app_management_role_definition" + ResourceName = "graph_device_and_app_management_role_definition" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( diff --git a/internal/resources/identity_and_access/beta/conditional_access_policy/crud.go b/internal/resources/identity_and_access/beta/conditional_access_policy/crud.go index 6baa33b5..2265ee45 100644 --- a/internal/resources/identity_and_access/beta/conditional_access_policy/crud.go +++ b/internal/resources/identity_and_access/beta/conditional_access_policy/crud.go @@ -23,7 +23,7 @@ func (r *ConditionalAccessPolicyResource) Create(ctx context.Context, req resour return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Create, CreateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -74,7 +74,7 @@ func (r *ConditionalAccessPolicyResource) Read(ctx context.Context, req resource tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, state.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -113,7 +113,7 @@ func (r *ConditionalAccessPolicyResource) Update(ctx context.Context, req resour return } - ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, plan.Timeouts.Update, UpdateTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -161,7 +161,7 @@ func (r *ConditionalAccessPolicyResource) Delete(ctx context.Context, req resour return } - ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, data.Timeouts.Delete, DeleteTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/resources/identity_and_access/beta/conditional_access_policy/resource.go b/internal/resources/identity_and_access/beta/conditional_access_policy/resource.go index ebaf9db8..e26d735c 100644 --- a/internal/resources/identity_and_access/beta/conditional_access_policy/resource.go +++ b/internal/resources/identity_and_access/beta/conditional_access_policy/resource.go @@ -19,7 +19,11 @@ import ( ) const ( - ResourceName = "graph_beta_identity_and_access_conditional_access_policy" + ResourceName = "graph_beta_identity_and_access_conditional_access_policy" + CreateTimeout = 180 + UpdateTimeout = 180 + ReadTimeout = 180 + DeleteTimeout = 180 ) var ( From ed90be9f86f2cc1def8cd00d2fd031d861c014fa Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 16:49:33 +0000 Subject: [PATCH 10/13] Refactor Read operations in multiple data sources to use resource.ReadTimeout for timeout handling; improve code clarity with added comments. --- .../beta/assignment_filter/read.go | 3 ++- .../beta/windows_platform_script/read.go | 3 ++- .../v1.0/cloud_pc_device_image/read.go | 8 ++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/datasources/device_and_app_management/beta/assignment_filter/read.go b/internal/datasources/device_and_app_management/beta/assignment_filter/read.go index 22711270..e0a66da6 100644 --- a/internal/datasources/device_and_app_management/beta/assignment_filter/read.go +++ b/internal/datasources/device_and_app_management/beta/assignment_filter/read.go @@ -12,6 +12,7 @@ import ( betamodels "github.com/microsoftgraph/msgraph-beta-sdk-go/models" ) +// Read handles the Read operation for the AssignmentFilterDataSource. func (d *AssignmentFilterDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var state resource.AssignmentFilterResourceModel resp.Diagnostics.Append(req.Config.Get(ctx, &state)...) @@ -19,7 +20,7 @@ func (d *AssignmentFilterDataSource) Read(ctx context.Context, req datasource.Re return } - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, resource.ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/datasources/device_and_app_management/beta/windows_platform_script/read.go b/internal/datasources/device_and_app_management/beta/windows_platform_script/read.go index 10237cea..af348baf 100644 --- a/internal/datasources/device_and_app_management/beta/windows_platform_script/read.go +++ b/internal/datasources/device_and_app_management/beta/windows_platform_script/read.go @@ -17,6 +17,7 @@ var ( object resource.WindowsPlatformScriptResourceModel ) +// Read handles the Read operation for the WindowsPlatformScriptDataSource. func (d *WindowsPlatformScriptDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { tflog.Debug(ctx, fmt.Sprintf("Starting Read method for: %s_%s", d.ProviderTypeName, d.TypeName)) @@ -28,7 +29,7 @@ func (d *WindowsPlatformScriptDataSource) Read(ctx context.Context, req datasour tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", d.ProviderTypeName, d.TypeName, object.ID.ValueString())) - ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Read, 30*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Read, resource.ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } diff --git a/internal/datasources/device_and_app_management/v1.0/cloud_pc_device_image/read.go b/internal/datasources/device_and_app_management/v1.0/cloud_pc_device_image/read.go index 5cf9153f..bb61d315 100644 --- a/internal/datasources/device_and_app_management/v1.0/cloud_pc_device_image/read.go +++ b/internal/datasources/device_and_app_management/v1.0/cloud_pc_device_image/read.go @@ -12,6 +12,7 @@ import ( models "github.com/microsoftgraph/msgraph-sdk-go/models" ) +// Read handles the Read operation for the CloudPcDeviceImageDataSource. func (d *CloudPcDeviceImageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var state resource.CloudPcDeviceImageResourceModel resp.Diagnostics.Append(req.Config.Get(ctx, &state)...) @@ -19,7 +20,7 @@ func (d *CloudPcDeviceImageDataSource) Read(ctx context.Context, req datasource. return } - ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, ReadTimeout*time.Second, &resp.Diagnostics) + ctx, cancel := crud.HandleTimeout(ctx, state.Timeouts.Read, resource.ReadTimeout*time.Second, &resp.Diagnostics) if cancel == nil { return } @@ -27,7 +28,10 @@ func (d *CloudPcDeviceImageDataSource) Read(ctx context.Context, req datasource. tflog.Debug(ctx, fmt.Sprintf("Reading assignment filter with display name: %s", state.DisplayName.ValueString())) - filters := d.client.DeviceManagement().VirtualEndpoint().DeviceImages() + filters := d.client. + DeviceManagement(). + VirtualEndpoint(). + DeviceImages() result, err := filters.Get(ctx, nil) if err != nil { resp.Diagnostics.AddError( From e53cfee988d3b333dac6d5983f58d9cd36926ab8 Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 17:03:01 +0000 Subject: [PATCH 11/13] Add script to delete settings catalog configurations by name prefix; includes functions for pagination, policy retrieval, and deletion with user confirmation. --- ...tingsCatalogConfigurationsByNamePrefix.ps1 | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 scripts/DeleteAllSettingsCatalogConfigurationsByNamePrefix.ps1 diff --git a/scripts/DeleteAllSettingsCatalogConfigurationsByNamePrefix.ps1 b/scripts/DeleteAllSettingsCatalogConfigurationsByNamePrefix.ps1 new file mode 100644 index 00000000..fec4f48e --- /dev/null +++ b/scripts/DeleteAllSettingsCatalogConfigurationsByNamePrefix.ps1 @@ -0,0 +1,130 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory=$true, + HelpMessage="Specify the Entra ID tenant ID (Directory ID) where the application is registered")] + [ValidateNotNullOrEmpty()] + [string]$TenantId, + + [Parameter(Mandatory=$true, + HelpMessage="Specify the application (client) ID of the Entra ID app registration")] + [ValidateNotNullOrEmpty()] + [string]$ClientId, + + [Parameter(Mandatory=$true, + HelpMessage="Specify the client secret of the Entra ID app registration")] + [ValidateNotNullOrEmpty()] + [string]$ClientSecret, + + [Parameter(Mandatory=$true, + HelpMessage="Specify the settings catalog name prefix to match for deletion (e.g., 'test_collection-')")] + [ValidateNotNullOrEmpty()] + [string]$SettingsCatalogNamePrefix +) + +# Helper function to retrieve all pages of items +function Get-Paginated { + param ( + [Parameter(Mandatory=$true)] + [string]$InitialUri + ) + + $allItems = @() + $currentUri = $InitialUri + + do { + $response = Invoke-MgGraphRequest -Method GET -Uri $currentUri + + if ($response.value) { + $allItems += $response.value + } + + # Get the next page URL if it exists + $currentUri = $response.'@odata.nextLink' + } while ($currentUri) + + return $allItems +} + +# Helper function to get all settings catalog policies +function Get-AllSettingsCatalogPolicies { + try { + $policiesUri = "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies" + return Get-Paginated -InitialUri $policiesUri + } + catch { + Write-Error "Error retrieving settings catalog policies: $_" + return $null + } +} + +# Helper function to delete a settings catalog policy +function Remove-SettingsCatalogPolicy { + param ( + [Parameter(Mandatory=$true)] + [string]$PolicyId, + [string]$PolicyName + ) + + try { + $policyUri = "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies/$PolicyId" + Invoke-MgGraphRequest -Method DELETE -Uri $policyUri + Write-Host "Successfully deleted policy: $PolicyName (ID: $PolicyId)" + return $true + } + catch { + Write-Error "Error deleting policy $PolicyName (ID: $PolicyId): $_" + return $false + } +} + +# Script Setup +Import-Module Microsoft.Graph.Authentication + +$secureClientSecret = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force +$clientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ClientId, $secureClientSecret + +Write-Host "Connecting to Microsoft Graph..." +Connect-MgGraph -ClientSecretCredential $clientSecretCredential -TenantId $TenantId + +# Get all settings catalog policies +Write-Host "Retrieving all settings catalog policies..." +$allPolicies = Get-AllSettingsCatalogPolicies + +if ($null -ne $allPolicies) { + # Filter policies by name prefix + $matchingPolicies = $allPolicies | Where-Object { $_.name -like "$SettingsCatalogNamePrefix*" } + + if ($matchingPolicies.Count -gt 0) { + Write-Host "`nFound $($matchingPolicies.Count) policies matching prefix '$SettingsCatalogNamePrefix'" + + # Confirm before deletion + $confirmation = Read-Host "Do you want to proceed with deletion? (Y/N)" + if ($confirmation -eq 'Y') { + $deletedCount = 0 + $failedCount = 0 + + foreach ($policy in $matchingPolicies) { + Write-Host "`nDeleting policy: $($policy.name)..." + $result = Remove-SettingsCatalogPolicy -PolicyId $policy.id -PolicyName $policy.name + if ($result) { + $deletedCount++ + } else { + $failedCount++ + } + } + + Write-Host "`nDeletion complete:" + Write-Host "Successfully deleted: $deletedCount" + Write-Host "Failed to delete: $failedCount" + } else { + Write-Host "Operation cancelled by user." + } + } else { + Write-Host "No policies found matching prefix '$SettingsCatalogNamePrefix'" + } +} else { + Write-Host "No settings catalog policies found or error occurred." +} + +Disconnect-MgGraph +Write-Host "Disconnected from Microsoft Graph." \ No newline at end of file From 0ab7da9cd67e77b838aaf84577abfef1ce49ffb4 Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 17:06:04 +0000 Subject: [PATCH 12/13] Remove SettingsCatalogResource implementation; eliminate unused code and improve repository clarity. --- .../beta/settings_catalog/crud.go.back | 344 ------------------ .../{resource_v7.go => resource.go} | 0 2 files changed, 344 deletions(-) delete mode 100644 internal/resources/device_and_app_management/beta/settings_catalog/crud.go.back rename internal/resources/device_and_app_management/beta/settings_catalog/{resource_v7.go => resource.go} (100%) diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go.back b/internal/resources/device_and_app_management/beta/settings_catalog/crud.go.back deleted file mode 100644 index 3ab86cb8..00000000 --- a/internal/resources/device_and_app_management/beta/settings_catalog/crud.go.back +++ /dev/null @@ -1,344 +0,0 @@ -package graphBetaSettingsCatalog - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/deploymenttheory/terraform-provider-microsoft365/internal/client/graphcustom" - "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/crud" - "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/errors" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -var ( - // mutex needed to lock Create requests during parallel runs to avoid overwhelming api and resulting in stating issues - mu sync.Mutex - - // object is the resource model for the Endpoint Privilege Management resource - object SettingsCatalogProfileResourceModel -) - -// Create handles the Create operation for Settings Catalog resources. -// -// - Retrieves the planned configuration from the create request -// - Constructs the resource request body from the plan -// - Sends POST request to create the base resource and settings -// - Captures the new resource ID from the response -// - Constructs and sends assignment configuration if specified -// - Sets initial state with planned values -// - Calls Read operation to fetch the latest state from the API -// - Updates the final state with the fresh data from the API -// -// The function ensures that both the settings catalog profile and its assignments -// (if specified) are created properly. The settings must be defined during creation -// as they are required for a successful deployment, while assignments are optional. -func (r *SettingsCatalogResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - - mu.Lock() - defer mu.Unlock() - - tflog.Debug(ctx, fmt.Sprintf("Starting creation of resource: %s_%s", r.ProviderTypeName, r.TypeName)) - - resp.Diagnostics.Append(req.Plan.Get(ctx, &object)...) - if resp.Diagnostics.HasError() { - return - } - - ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Create, 30*time.Second, &resp.Diagnostics) - if cancel == nil { - return - } - defer cancel() - - requestBody, err := constructResource(ctx, &object) - if err != nil { - resp.Diagnostics.AddError( - "Error constructing resource for Create method", - fmt.Sprintf("Could not construct resource: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()), - ) - return - } - - requestResource, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - Post(context.Background(), requestBody, nil) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Create", r.WritePermissions) - return - } - - object.ID = types.StringValue(*requestResource.GetId()) - - if object.Assignments != nil { - requestAssignment, err := constructAssignment(ctx, &object) - if err != nil { - resp.Diagnostics.AddError( - "Error constructing assignment for create method", - fmt.Sprintf("Could not construct assignment: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()), - ) - return - } - - _, err = r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Assign(). - Post(ctx, requestAssignment, nil) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Create", r.WritePermissions) - return - } - } - - resp.Diagnostics.Append(resp.State.Set(ctx, &object)...) - if resp.Diagnostics.HasError() { - return - } - - readResp := &resource.ReadResponse{ - State: resp.State, - } - r.Read(ctx, resource.ReadRequest{ - State: resp.State, - ProviderMeta: req.ProviderMeta, - }, readResp) - - resp.Diagnostics.Append(readResp.Diagnostics...) - if resp.Diagnostics.HasError() { - return - } - - resp.State = readResp.State - - tflog.Debug(ctx, fmt.Sprintf("Finished Update Method: %s_%s", r.ProviderTypeName, r.TypeName)) -} - -// Read handles the Read operation for Settings Catalog resources. -// -// - Retrieves the current state from the read request -// - Gets the base resource details from the API -// - Maps the base resource details to Terraform state -// - Gets the settings configuration from the API -// - Maps the settings configuration to Terraform state -// - Gets the assignments configuration from the API -// - Maps the assignments configuration to Terraform state -// -// The function ensures that all components (base resource, settings, and assignments) -// are properly read and mapped into the Terraform state, providing a complete view -// of the resource's current configuration on the server. -func (r *SettingsCatalogResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - - tflog.Debug(ctx, fmt.Sprintf("Starting Read method for: %s_%s", r.ProviderTypeName, r.TypeName)) - - resp.Diagnostics.Append(req.State.Get(ctx, &object)...) - if resp.Diagnostics.HasError() { - return - } - - tflog.Debug(ctx, fmt.Sprintf("Reading %s_%s with ID: %s", r.ProviderTypeName, r.TypeName, object.ID.ValueString())) - - ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Read, 30*time.Second, &resp.Diagnostics) - if cancel == nil { - return - } - defer cancel() - - respResource, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Get(ctx, nil) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Read", r.ReadPermissions) - return - } - - MapRemoteResourceStateToTerraform(ctx, &object, respResource) - - settingsConfig := graphcustom.GetRequestConfig{ - APIVersion: graphcustom.GraphAPIBeta, - Endpoint: r.ResourcePath, - EndpointSuffix: "/settings", - ResourceIDPattern: "('id')", - ResourceID: object.ID.ValueString(), - QueryParameters: map[string]string{ - "$expand": "children", - }, - } - - respSettings, err := graphcustom.GetRequestByResourceId( - ctx, - r.client.GetAdapter(), - settingsConfig, - ) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Create - Settings Fetch", r.ReadPermissions) - return - } - - MapRemoteSettingsStateToTerraform(ctx, &object, respSettings) - - respAssignments, err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Assignments(). - Get(context.Background(), nil) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Read", r.ReadPermissions) - return - } - - MapRemoteAssignmentStateToTerraform(ctx, &object, respAssignments) - - resp.Diagnostics.Append(resp.State.Set(ctx, &object)...) - if resp.Diagnostics.HasError() { - return - } - - tflog.Debug(ctx, fmt.Sprintf("Finished Read Method: %s_%s", r.ProviderTypeName, r.TypeName)) -} - -// Update handles the Update operation for Settings Catalog resources. -// -// - Retrieves the planned changes from the update request -// - Constructs the resource request body from the plan -// - Sends PUT request to update the base resource and settings -// - Constructs the assignment request body from the plan -// - Sends POST request to update the assignments -// - Sets initial state with planned values -// - Calls Read operation to fetch the latest state from the API -// - Updates the final state with the fresh data from the API -// -// The function ensures that both the settings and assignments are updated atomically, -// and the final state reflects the actual state of the resource on the server. -func (r *SettingsCatalogResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - - tflog.Debug(ctx, fmt.Sprintf("Starting Update of resource: %s_%s", r.ProviderTypeName, r.TypeName)) - - resp.Diagnostics.Append(req.Plan.Get(ctx, &object)...) - if resp.Diagnostics.HasError() { - return - } - - ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Update, 30*time.Second, &resp.Diagnostics) - if cancel == nil { - return - } - defer cancel() - - requestBody, err := constructResource(ctx, &object) - if err != nil { - resp.Diagnostics.AddError( - "Error constructing resource for Update method", - fmt.Sprintf("Could not construct resource: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()), - ) - return - } - - putRequest := graphcustom.PutRequestConfig{ - APIVersion: graphcustom.GraphAPIBeta, - Endpoint: r.ResourcePath, - ResourceID: object.ID.ValueString(), - RequestBody: requestBody, - } - - err = graphcustom.PutRequestByResourceId(ctx, r.client.GetAdapter(), putRequest) - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Update", r.ReadPermissions) - return - } - - requestAssignment, err := constructAssignment(ctx, &object) - if err != nil { - resp.Diagnostics.AddError( - "Error constructing assignment for update method", - fmt.Sprintf("Could not construct assignment: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()), - ) - return - } - - _, err = r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Assign(). - Post(ctx, requestAssignment, nil) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Update", r.WritePermissions) - return - } - - resp.Diagnostics.Append(resp.State.Set(ctx, &object)...) - if resp.Diagnostics.HasError() { - return - } - - readResp := &resource.ReadResponse{ - State: resp.State, - } - r.Read(ctx, resource.ReadRequest{ - State: resp.State, - ProviderMeta: req.ProviderMeta, - }, readResp) - - resp.Diagnostics.Append(readResp.Diagnostics...) - if resp.Diagnostics.HasError() { - return - } - - resp.State = readResp.State - - tflog.Debug(ctx, fmt.Sprintf("Finished Update Method: %s_%s", r.ProviderTypeName, r.TypeName)) -} - -// Delete handles the Delete operation for Settings Catalog resources. -// -// - Retrieves the current state from the delete request -// - Validates the state data and timeout configuration -// - Sends DELETE request to remove the resource from the API -// - Cleans up by removing the resource from Terraform state -// -// All assignments and settings associated with the resource are automatically removed as part of the deletion. -func (r *SettingsCatalogResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - - tflog.Debug(ctx, fmt.Sprintf("Starting deletion of resource: %s_%s", r.ProviderTypeName, r.TypeName)) - - resp.Diagnostics.Append(req.State.Get(ctx, &object)...) - if resp.Diagnostics.HasError() { - return - } - - ctx, cancel := crud.HandleTimeout(ctx, object.Timeouts.Delete, 30*time.Second, &resp.Diagnostics) - if cancel == nil { - return - } - defer cancel() - - err := r.client. - DeviceManagement(). - ConfigurationPolicies(). - ByDeviceManagementConfigurationPolicyId(object.ID.ValueString()). - Delete(ctx, nil) - - if err != nil { - errors.HandleGraphError(ctx, err, resp, "Delete", r.ReadPermissions) - return - } - - tflog.Debug(ctx, fmt.Sprintf("Finished Delete Method: %s_%s", r.ProviderTypeName, r.TypeName)) - - resp.State.RemoveResource(ctx) -} diff --git a/internal/resources/device_and_app_management/beta/settings_catalog/resource_v7.go b/internal/resources/device_and_app_management/beta/settings_catalog/resource.go similarity index 100% rename from internal/resources/device_and_app_management/beta/settings_catalog/resource_v7.go rename to internal/resources/device_and_app_management/beta/settings_catalog/resource.go From b207c2df2dcdd2c42f3bd7ef73df5dc589c21cde Mon Sep 17 00:00:00 2001 From: ShocOne Date: Sun, 1 Dec 2024 17:09:56 +0000 Subject: [PATCH 13/13] Update documentation for Microsoft 365 provider; refine subcategory labels and improve clarity in descriptions. --- docs/index.md | 6 +++--- ...raph_beta_device_and_app_management_assignment_filter.md | 2 +- ..._beta_device_and_app_management_macos_platform_script.md | 2 +- ...graph_beta_device_and_app_management_settings_catalog.md | 2 +- ...eta_device_and_app_management_windows_platform_script.md | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/index.md b/docs/index.md index dd0c0d6c..f74af795 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. @@ -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 = { @@ -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" { diff --git a/docs/resources/graph_beta_device_and_app_management_assignment_filter.md b/docs/resources/graph_beta_device_and_app_management_assignment_filter.md index 82401c6c..2c4c3612 100644 --- a/docs/resources/graph_beta_device_and_app_management_assignment_filter.md +++ b/docs/resources/graph_beta_device_and_app_management_assignment_filter.md @@ -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. --- diff --git a/docs/resources/graph_beta_device_and_app_management_macos_platform_script.md b/docs/resources/graph_beta_device_and_app_management_macos_platform_script.md index 04408f77..2550c9e0 100644 --- a/docs/resources/graph_beta_device_and_app_management_macos_platform_script.md +++ b/docs/resources/graph_beta_device_and_app_management_macos_platform_script.md @@ -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. --- diff --git a/docs/resources/graph_beta_device_and_app_management_settings_catalog.md b/docs/resources/graph_beta_device_and_app_management_settings_catalog.md index b9096ecf..67abed0d 100644 --- a/docs/resources/graph_beta_device_and_app_management_settings_catalog.md +++ b/docs/resources/graph_beta_device_and_app_management_settings_catalog.md @@ -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. --- diff --git a/docs/resources/graph_beta_device_and_app_management_windows_platform_script.md b/docs/resources/graph_beta_device_and_app_management_windows_platform_script.md index 65758dc5..9c41e4ed 100644 --- a/docs/resources/graph_beta_device_and_app_management_windows_platform_script.md +++ b/docs/resources/graph_beta_device_and_app_management_windows_platform_script.md @@ -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. ---