diff --git a/docs/data-sources/objectstorage_compliance_lock.md b/docs/data-sources/objectstorage_compliance_lock.md index ff06c77f6..55009f50b 100644 --- a/docs/data-sources/objectstorage_compliance_lock.md +++ b/docs/data-sources/objectstorage_compliance_lock.md @@ -3,12 +3,12 @@ page_title: "stackit_objectstorage_compliance_lock Data Source - stackit" subcategory: "" description: |- - ObjectStorage compliance lock resource schema. Must have a region specified in the provider configuration. + ObjectStorage compliance lock data source schema. Must have a region specified in the provider configuration. --- # stackit_objectstorage_compliance_lock (Data Source) -ObjectStorage compliance lock resource schema. Must have a `region` specified in the provider configuration. +ObjectStorage compliance lock data source schema. Must have a `region` specified in the provider configuration. ## Example Usage diff --git a/stackit/internal/services/objectstorage/bucket/datasource.go b/stackit/internal/services/objectstorage/bucket/datasource.go index 9130ef6c9..861af48ff 100644 --- a/stackit/internal/services/objectstorage/bucket/datasource.go +++ b/stackit/internal/services/objectstorage/bucket/datasource.go @@ -130,7 +130,11 @@ func (r *bucketDataSource) Read(ctx context.Context, req datasource.ReadRequest, ctx = tflog.SetField(ctx, "name", bucketName) ctx = tflog.SetField(ctx, "region", region) - bucketResp, err := r.client.DefaultAPI.GetBucket(ctx, projectId, region, bucketName).Execute() + bucketResp, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.GetBucket(ctx, projectId, region, bucketName).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { utils.LogError( ctx, @@ -139,7 +143,8 @@ func (r *bucketDataSource) Read(ctx context.Context, req datasource.ReadRequest, "Reading bucket", fmt.Sprintf("Bucket with name %q does not exist in project %q.", bucketName, projectId), map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), + http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), + http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg, }, ) resp.State.RemoveResource(ctx) diff --git a/stackit/internal/services/objectstorage/bucket/resource.go b/stackit/internal/services/objectstorage/bucket/resource.go index 7bcb1cab5..242e9042b 100644 --- a/stackit/internal/services/objectstorage/bucket/resource.go +++ b/stackit/internal/services/objectstorage/bucket/resource.go @@ -218,9 +218,15 @@ func (r *bucketResource) Create(ctx context.Context, req resource.CreateRequest, } // Create new bucket - _, err = r.client.DefaultAPI.CreateBucket(ctx, projectId, region, bucketName).ObjectLockEnabled(model.ObjectLock.ValueBool()).Execute() + _, err = utils.RetryRequest( + ctx, + r.client.DefaultAPI.CreateBucket(ctx, projectId, region, bucketName).ObjectLockEnabled(model.ObjectLock.ValueBool()).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error creating bucket", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } @@ -275,14 +281,20 @@ func (r *bucketResource) Read(ctx context.Context, req resource.ReadRequest, res ctx = tflog.SetField(ctx, "name", bucketName) ctx = tflog.SetField(ctx, "region", region) - bucketResp, err := r.client.DefaultAPI.GetBucket(ctx, projectId, region, bucketName).Execute() + bucketResp, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.GetBucket(ctx, projectId, region, bucketName).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bucket", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error reading bucket", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } @@ -330,7 +342,11 @@ func (r *bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, ctx = tflog.SetField(ctx, "region", region) // Delete existing bucket - _, err := r.client.DefaultAPI.DeleteBucket(ctx, projectId, region, bucketName).Execute() + _, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.DeleteBucket(ctx, projectId, region, bucketName).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) { @@ -343,7 +359,9 @@ func (r *bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, return } } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bucket", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error deleting bucket", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) } ctx = core.LogResponse(ctx) diff --git a/stackit/internal/services/objectstorage/compliance-lock/datasource.go b/stackit/internal/services/objectstorage/compliance-lock/datasource.go index 55ea6dfd4..a4b9d1325 100644 --- a/stackit/internal/services/objectstorage/compliance-lock/datasource.go +++ b/stackit/internal/services/objectstorage/compliance-lock/datasource.go @@ -58,7 +58,7 @@ func (d *compliancelockDataSource) Configure(ctx context.Context, req datasource // Schema defines the schema for the resource. func (d *compliancelockDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { descriptions := map[string]string{ - "main": "ObjectStorage compliance lock resource schema. Must have a `region` specified in the provider configuration.", + "main": "ObjectStorage compliance lock data source schema. Must have a `region` specified in the provider configuration.", "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`\".", "project_id": "STACKIT Project ID to which the compliance lock is associated.", "region": "The resource region. If not defined, the provider region is used.", @@ -110,7 +110,11 @@ func (d *compliancelockDataSource) Read(ctx context.Context, req datasource.Read ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) - complianceResp, err := d.client.DefaultAPI.GetComplianceLock(ctx, projectId, region).Execute() + complianceResp, err := utils.RetryRequest( + ctx, + d.client.DefaultAPI.GetComplianceLock(ctx, projectId, region).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { utils.LogError( ctx, @@ -119,7 +123,8 @@ func (d *compliancelockDataSource) Read(ctx context.Context, req datasource.Read "Reading compliance lock", fmt.Sprintf("Compliance lock does not exist in project %q.", projectId), map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), + http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), + http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg, }, ) resp.State.RemoveResource(ctx) diff --git a/stackit/internal/services/objectstorage/compliance-lock/resource.go b/stackit/internal/services/objectstorage/compliance-lock/resource.go index be56edde9..6475fd42e 100644 --- a/stackit/internal/services/objectstorage/compliance-lock/resource.go +++ b/stackit/internal/services/objectstorage/compliance-lock/resource.go @@ -165,20 +165,32 @@ func (r *compliancelockResource) Create(ctx context.Context, req resource.Create ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) - complianceResp, err := r.client.DefaultAPI.CreateComplianceLock(ctx, projectId, region).Execute() + complianceResp, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.CreateComplianceLock(ctx, projectId, region).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { var oapiErr *oapierror.GenericOpenAPIError ok := errors.As(err, &oapiErr) if !(ok && oapiErr.StatusCode == http.StatusConflict) { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating compliance lock", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error creating compliance lock", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } tflog.Info(ctx, "Compliance lock is already enabled for this project. Please check duplicate resources.") - complianceResp, err = r.client.DefaultAPI.GetComplianceLock(ctx, projectId, region).Execute() + complianceResp, err = utils.RetryRequest( + ctx, + r.client.DefaultAPI.GetComplianceLock(ctx, projectId, region).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading compliance lock", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error reading compliance lock", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } } @@ -216,14 +228,20 @@ func (r *compliancelockResource) Read(ctx context.Context, req resource.ReadRequ ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) - complianceResp, err := r.client.DefaultAPI.GetComplianceLock(ctx, projectId, region).Execute() + complianceResp, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.GetComplianceLock(ctx, projectId, region).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading compliance lock", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error reading compliance lock", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } @@ -268,9 +286,15 @@ func (r *compliancelockResource) Delete(ctx context.Context, req resource.Delete ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) - _, err := r.client.DefaultAPI.DeleteComplianceLock(ctx, projectId, region).Execute() + _, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.DeleteComplianceLock(ctx, projectId, region).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting compliance lock", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error deleting compliance lock", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } diff --git a/stackit/internal/services/objectstorage/credential/datasource.go b/stackit/internal/services/objectstorage/credential/datasource.go index 9d8b96a3b..274c64425 100644 --- a/stackit/internal/services/objectstorage/credential/datasource.go +++ b/stackit/internal/services/objectstorage/credential/datasource.go @@ -133,7 +133,11 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ ctx = tflog.SetField(ctx, "credential_id", credentialId) ctx = tflog.SetField(ctx, "region", region) - credentialsGroupResp, err := r.client.DefaultAPI.ListAccessKeys(ctx, projectId, region).CredentialsGroup(credentialsGroupId).Execute() + credentialsGroupResp, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.ListAccessKeys(ctx, projectId, region).CredentialsGroup(credentialsGroupId).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { utils.LogError( ctx, @@ -142,7 +146,8 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ "Reading credential", fmt.Sprintf("Credential group with ID %q does not exist in project %q.", credentialsGroupId, projectId), map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), + http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), + http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg, }, ) resp.State.RemoveResource(ctx) diff --git a/stackit/internal/services/objectstorage/credential/resource.go b/stackit/internal/services/objectstorage/credential/resource.go index 6321d5e15..4a083a22f 100644 --- a/stackit/internal/services/objectstorage/credential/resource.go +++ b/stackit/internal/services/objectstorage/credential/resource.go @@ -294,9 +294,15 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ return } // Create new credential - credentialResp, err := r.client.DefaultAPI.CreateAccessKey(ctx, projectId, region).CredentialsGroup(credentialsGroupId).CreateAccessKeyPayload(*payload).Execute() + credentialResp, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.CreateAccessKey(ctx, projectId, region).CredentialsGroup(credentialsGroupId).CreateAccessKeyPayload(*payload).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error creating credential", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } @@ -377,7 +383,9 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, found, err := readCredentials(ctx, &model, region, r.client) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error reading credential", fmt.Sprintf("Finding credential: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } @@ -454,13 +462,19 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ ctx = tflog.SetField(ctx, "region", region) // Delete existing credential - _, err := r.client.DefaultAPI.DeleteAccessKey(ctx, projectId, region, credentialId).CredentialsGroup(credentialsGroupId).Execute() + _, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.DeleteAccessKey(ctx, projectId, region, credentialId).CredentialsGroup(credentialsGroupId).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { return } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error deleting credential", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } @@ -560,7 +574,11 @@ func readCredentials(ctx context.Context, model *Model, region string, client *o credentialsGroupId := model.CredentialsGroupId.ValueString() credentialId := model.CredentialId.ValueString() - credentialsGroupResp, err := client.DefaultAPI.ListAccessKeys(ctx, projectId, region).CredentialsGroup(credentialsGroupId).Execute() + credentialsGroupResp, err := utils.RetryRequest( + ctx, + client.DefaultAPI.ListAccessKeys(ctx, projectId, region).CredentialsGroup(credentialsGroupId).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { diff --git a/stackit/internal/services/objectstorage/credentialsgroup/resource.go b/stackit/internal/services/objectstorage/credentialsgroup/resource.go index 6d69d0413..41f036f66 100644 --- a/stackit/internal/services/objectstorage/credentialsgroup/resource.go +++ b/stackit/internal/services/objectstorage/credentialsgroup/resource.go @@ -199,9 +199,15 @@ func (r *credentialsGroupResource) Create(ctx context.Context, req resource.Crea } // Create new credentials group - got, err := r.client.DefaultAPI.CreateCredentialsGroup(ctx, projectId, region).CreateCredentialsGroupPayload(createCredentialsGroupPayload).Execute() + got, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.CreateCredentialsGroup(ctx, projectId, region).CreateCredentialsGroupPayload(createCredentialsGroupPayload).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials group", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error creating credentials group", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } @@ -262,7 +268,9 @@ func (r *credentialsGroupResource) Read(ctx context.Context, req resource.ReadRe found, err := readCredentialsGroups(ctx, &model, region, r.client.DefaultAPI) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentialsGroup", fmt.Sprintf("getting credential group from list of credentials groups: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error reading credentialsGroup", fmt.Sprintf("getting credential group from list of credentials groups: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } @@ -310,14 +318,20 @@ func (r *credentialsGroupResource) Delete(ctx context.Context, req resource.Dele ctx = tflog.SetField(ctx, "region", region) // Delete existing credentials group - _, err := r.client.DefaultAPI.DeleteCredentialsGroup(ctx, projectId, region, credentialsGroupId).Execute() + _, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.DeleteCredentialsGroup(ctx, projectId, region, credentialsGroupId).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials group", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error deleting credentials group", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } @@ -390,7 +404,11 @@ func readCredentialsGroups(ctx context.Context, model *Model, region string, cli return found, fmt.Errorf("missing configuration: either name or credentials group id must be provided") } - credentialsGroupsResp, err := client.ListCredentialsGroups(ctx, model.ProjectId.ValueString(), region).Execute() + credentialsGroupsResp, err := utils.RetryRequest( + ctx, + client.ListCredentialsGroups(ctx, model.ProjectId.ValueString(), region).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { diff --git a/stackit/internal/services/objectstorage/default-retention/datasource.go b/stackit/internal/services/objectstorage/default-retention/datasource.go index 395c67b6e..bda677be2 100644 --- a/stackit/internal/services/objectstorage/default-retention/datasource.go +++ b/stackit/internal/services/objectstorage/default-retention/datasource.go @@ -16,6 +16,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -110,14 +111,20 @@ func (d *defaultRetentionDataSource) Read(ctx context.Context, req datasource.Re ctx = tflog.SetField(ctx, "region", region) // Read default-retention - result, err := d.client.DefaultAPI.GetDefaultRetention(ctx, projectId, region, bucketName).Execute() + result, err := utils.RetryRequest( + ctx, + d.client.DefaultAPI.GetDefaultRetention(ctx, projectId, region, bucketName).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading default-retention", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error reading default-retention", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } diff --git a/stackit/internal/services/objectstorage/default-retention/resource.go b/stackit/internal/services/objectstorage/default-retention/resource.go index abdd0afb0..0af2272d9 100644 --- a/stackit/internal/services/objectstorage/default-retention/resource.go +++ b/stackit/internal/services/objectstorage/default-retention/resource.go @@ -207,13 +207,15 @@ func (r *defaultRetentionResource) Create(ctx context.Context, req resource.Crea if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", fmt.Sprintf("Parsing model: %v", err)) } - result, err := apiRequest.Execute() + result, err := utils.RetryRequest(ctx, apiRequest.Execute, objectstorageUtils.RateLimitRetryConfig) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error setting default-retention", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } - ctx = core.LogResponse(ctx) + ctx = core.LogResponse(ctx) //nolint:tflogresponse // false positive - SDK call is done inside utils.RetryRequest err = mapFields(result, &model, region) if err != nil { @@ -248,7 +250,11 @@ func (r *defaultRetentionResource) Delete(ctx context.Context, req resource.Dele ctx = tflog.SetField(ctx, "region", region) // Delete default-retention - _, err := r.client.DefaultAPI.DeleteDefaultRetention(ctx, projectId, region, bucketName).Execute() + _, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.DeleteDefaultRetention(ctx, projectId, region, bucketName).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok { if oapiErr.StatusCode == http.StatusNotFound { @@ -260,7 +266,9 @@ func (r *defaultRetentionResource) Delete(ctx context.Context, req resource.Dele return } } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting default-retention", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error deleting default-retention", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } @@ -287,14 +295,20 @@ func (r *defaultRetentionResource) Read(ctx context.Context, req resource.ReadRe ctx = tflog.SetField(ctx, "region", region) // Read default-retention - result, err := r.client.DefaultAPI.GetDefaultRetention(ctx, projectId, region, bucketName).Execute() + result, err := utils.RetryRequest( + ctx, + r.client.DefaultAPI.GetDefaultRetention(ctx, projectId, region, bucketName).Execute, + objectstorageUtils.RateLimitRetryConfig, + ) if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading default-retention", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error reading default-retention", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } @@ -337,13 +351,15 @@ func (r *defaultRetentionResource) Update(ctx context.Context, req resource.Upda if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", fmt.Sprintf("Parsing model: %v", err)) } - result, err := apiRequest.Execute() + result, err := utils.RetryRequest(ctx, apiRequest.Execute, objectstorageUtils.RateLimitRetryConfig) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", fmt.Sprintf("Calling API: %v", err)) + utils.LogError(ctx, &resp.Diagnostics, err, "Error setting default-retention", fmt.Sprintf("Calling API: %v", err), + map[int]string{http.StatusTooManyRequests: objectstorageUtils.RateLimitErrMsg}, + ) return } - ctx = core.LogResponse(ctx) + ctx = core.LogResponse(ctx) //nolint:tflogresponse // false positive - SDK call is done inside utils.RetryRequest err = mapFields(result, &model, region) if err != nil { diff --git a/stackit/internal/services/objectstorage/utils/util.go b/stackit/internal/services/objectstorage/utils/util.go index 8e3ab1a8a..2d7581949 100644 --- a/stackit/internal/services/objectstorage/utils/util.go +++ b/stackit/internal/services/objectstorage/utils/util.go @@ -3,6 +3,7 @@ package utils import ( "context" "fmt" + "math/rand" "net/http" "time" @@ -18,8 +19,37 @@ import ( const ( enableProjectAttempts = 4 enableProjectRetryDelay = 2 * time.Second + + // RateLimitErrMsg is the user-facing error shown when the Object Storage Control Plane + // rate limit (HTTP 429) is exceeded and all retry attempts are exhausted. + RateLimitErrMsg = "API rate limit exceeded. The Object Storage Control Plane limits concurrent " + + "requests; the provider retried but the limit was not resolved in time. " + + "Consider re-running or reducing the number of parallel resources with -parallelism." ) +// RateLimitRetryConfig retries on HTTP 429 with exponential backoff and jitter. +// +// Tuned for the Object Storage Control Plane rate limit (≥60 req/min): +// - 10s base start: at 60 req/min (1 req/s) this refills ~10 tokens, enough for all +// goroutines competing at Terraform's default parallelism of 10 to succeed on first retry. +// - ±25% jitter: spreads concurrent retries so they don't all hit the API at the same +// instant after the rate-limit window resets (thundering herd). The 60 allowed requests +// are typically consumed within a few seconds; without jitter all goroutines wake at the +// exact same moment ~50s later and collide again. +// - 60s cap: covers a full rate-limit window reset. +// - 15 attempts: total budget ≈790s, covering up to ~790 concurrent goroutines and +// well above the ~1000s run time for 1000 buckets at 60 req/min. +var RateLimitRetryConfig = utils.RetryConfig{ + Attempts: 15, + Backoff: func(attempt int) time.Duration { + base := min(10*time.Second*(1<