From 31e8ff586a20cacd14cb90cc084b644287cbb7c3 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Wed, 5 Aug 2026 16:51:45 +0200 Subject: [PATCH 1/2] fix(sqlserverflex): add retries on Locked + TooEarly for user + database STACKITTPR-785 --- .../sqlserverflex/database/resource.go | 31 +++++++++++++++++-- .../services/sqlserverflex/user/resource.go | 31 +++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/stackit/internal/services/sqlserverflex/database/resource.go b/stackit/internal/services/sqlserverflex/database/resource.go index e47852549..569107236 100644 --- a/stackit/internal/services/sqlserverflex/database/resource.go +++ b/stackit/internal/services/sqlserverflex/database/resource.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -202,7 +203,20 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques ctx = core.InitProviderContext(ctx) - _, err = r.client.DefaultAPI.CreateDatabase(ctx, projectId, region, instanceId).CreateDatabasePayload(*payload).Execute() + // Workaround: The database creation will be tried 5 times. In some cases the instance might be + // in maintenance mode and the database API is temporarily unavailable. Usually this is only for 1-2 seconds. + config := utils.RetryConfig{ + Attempts: 5, + Backoff: func(attempt int) time.Duration { + // Wait for every attempt 5 seconds longer. 5s, 10s, 15s and so on + return time.Duration(attempt*5) * time.Second + }, + RetryStatusCodes: []int{ + http.StatusLocked, + http.StatusTooEarly, + }, + } + _, err = utils.RetryRequest(ctx, r.client.DefaultAPI.CreateDatabase(ctx, projectId, region, instanceId).CreateDatabasePayload(*payload).Execute, config) if err != nil { resp.Diagnostics.AddError("Error creating database", err.Error()) return @@ -314,7 +328,20 @@ func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteReques ctx = core.InitProviderContext(ctx) - err := r.client.DefaultAPI.DeleteDatabase(ctx, projectId, region, instanceId, name).Execute() + // Workaround: The database deletion will be tried 5 times. In some cases the instance might be + // in maintenance mode and the database API is temporarily unavailable. Usually this is only for 1-2 seconds. + config := utils.RetryConfig{ + Attempts: 5, + Backoff: func(attempt int) time.Duration { + // Wait for every attempt 5 seconds longer. 5s, 10s, 15s and so on + return time.Duration(attempt*5) * time.Second + }, + RetryStatusCodes: []int{ + http.StatusLocked, + http.StatusTooEarly, + }, + } + err := utils.RetryRequestWithoutResponse(ctx, r.client.DefaultAPI.DeleteDatabase(ctx, projectId, region, instanceId, name).Execute, config) if err != nil { if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound { return diff --git a/stackit/internal/services/sqlserverflex/user/resource.go b/stackit/internal/services/sqlserverflex/user/resource.go index 6f2b11401..8b2f3f7f3 100644 --- a/stackit/internal/services/sqlserverflex/user/resource.go +++ b/stackit/internal/services/sqlserverflex/user/resource.go @@ -7,6 +7,7 @@ import ( "net/http" "strconv" "strings" + "time" "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" @@ -263,7 +264,20 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r return } // Create new user - userResp, err := r.client.DefaultAPI.CreateUser(ctx, projectId, region, instanceId).CreateUserPayload(*payload).Execute() + // Workaround: The user creation will be tried 5 times. In some cases the instance might be + // in maintenance mode and the user API is temporarily unavailable. Usually this is only for 1-2 seconds. + config := utils.RetryConfig{ + Attempts: 5, + Backoff: func(attempt int) time.Duration { + // Wait for every attempt 5 seconds longer. 5s, 10s, 15s and so on + return time.Duration(attempt*5) * time.Second + }, + RetryStatusCodes: []int{ + http.StatusLocked, + http.StatusTooEarly, + }, + } + userResp, err := utils.RetryRequest(ctx, r.client.DefaultAPI.CreateUser(ctx, projectId, region, instanceId).CreateUserPayload(*payload).Execute, config) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) return @@ -389,7 +403,20 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r } // Delete existing user - err = r.client.DefaultAPI.DeleteUser(ctx, projectId, region, instanceId, userId).Execute() + // Workaround: The user deletion will be tried 5 times. In some cases the instance might be + // in maintenance mode and the user API is temporarily unavailable. Usually this is only for 1-2 seconds. + config := utils.RetryConfig{ + Attempts: 5, + Backoff: func(attempt int) time.Duration { + // Wait for every attempt 5 seconds longer. 5s, 10s, 15s and so on + return time.Duration(attempt*5) * time.Second + }, + RetryStatusCodes: []int{ + http.StatusLocked, + http.StatusTooEarly, + }, + } + err = utils.RetryRequestWithoutResponse(ctx, r.client.DefaultAPI.DeleteUser(ctx, projectId, region, instanceId, userId).Execute, config) if err != nil { if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound { return From 8236ceeffe506eea9fef76627b50303ede2ffb48 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Wed, 5 Aug 2026 16:58:03 +0200 Subject: [PATCH 2/2] refac(sqlserverflex): extract retry config into variable --- .../sqlserverflex/database/resource.go | 27 ++----------------- .../services/sqlserverflex/user/resource.go | 27 ++----------------- .../services/sqlserverflex/utils/util.go | 14 ++++++++++ 3 files changed, 18 insertions(+), 50 deletions(-) diff --git a/stackit/internal/services/sqlserverflex/database/resource.go b/stackit/internal/services/sqlserverflex/database/resource.go index 569107236..1e6bacb93 100644 --- a/stackit/internal/services/sqlserverflex/database/resource.go +++ b/stackit/internal/services/sqlserverflex/database/resource.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "strings" - "time" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -205,18 +204,7 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques // Workaround: The database creation will be tried 5 times. In some cases the instance might be // in maintenance mode and the database API is temporarily unavailable. Usually this is only for 1-2 seconds. - config := utils.RetryConfig{ - Attempts: 5, - Backoff: func(attempt int) time.Duration { - // Wait for every attempt 5 seconds longer. 5s, 10s, 15s and so on - return time.Duration(attempt*5) * time.Second - }, - RetryStatusCodes: []int{ - http.StatusLocked, - http.StatusTooEarly, - }, - } - _, err = utils.RetryRequest(ctx, r.client.DefaultAPI.CreateDatabase(ctx, projectId, region, instanceId).CreateDatabasePayload(*payload).Execute, config) + _, err = utils.RetryRequest(ctx, r.client.DefaultAPI.CreateDatabase(ctx, projectId, region, instanceId).CreateDatabasePayload(*payload).Execute, sqlserverflexUtils.RetryConfig) if err != nil { resp.Diagnostics.AddError("Error creating database", err.Error()) return @@ -330,18 +318,7 @@ func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteReques // Workaround: The database deletion will be tried 5 times. In some cases the instance might be // in maintenance mode and the database API is temporarily unavailable. Usually this is only for 1-2 seconds. - config := utils.RetryConfig{ - Attempts: 5, - Backoff: func(attempt int) time.Duration { - // Wait for every attempt 5 seconds longer. 5s, 10s, 15s and so on - return time.Duration(attempt*5) * time.Second - }, - RetryStatusCodes: []int{ - http.StatusLocked, - http.StatusTooEarly, - }, - } - err := utils.RetryRequestWithoutResponse(ctx, r.client.DefaultAPI.DeleteDatabase(ctx, projectId, region, instanceId, name).Execute, config) + err := utils.RetryRequestWithoutResponse(ctx, r.client.DefaultAPI.DeleteDatabase(ctx, projectId, region, instanceId, name).Execute, sqlserverflexUtils.RetryConfig) if err != nil { if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound { return diff --git a/stackit/internal/services/sqlserverflex/user/resource.go b/stackit/internal/services/sqlserverflex/user/resource.go index 8b2f3f7f3..627a83dc8 100644 --- a/stackit/internal/services/sqlserverflex/user/resource.go +++ b/stackit/internal/services/sqlserverflex/user/resource.go @@ -7,7 +7,6 @@ import ( "net/http" "strconv" "strings" - "time" "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" @@ -266,18 +265,7 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r // Create new user // Workaround: The user creation will be tried 5 times. In some cases the instance might be // in maintenance mode and the user API is temporarily unavailable. Usually this is only for 1-2 seconds. - config := utils.RetryConfig{ - Attempts: 5, - Backoff: func(attempt int) time.Duration { - // Wait for every attempt 5 seconds longer. 5s, 10s, 15s and so on - return time.Duration(attempt*5) * time.Second - }, - RetryStatusCodes: []int{ - http.StatusLocked, - http.StatusTooEarly, - }, - } - userResp, err := utils.RetryRequest(ctx, r.client.DefaultAPI.CreateUser(ctx, projectId, region, instanceId).CreateUserPayload(*payload).Execute, config) + userResp, err := utils.RetryRequest(ctx, r.client.DefaultAPI.CreateUser(ctx, projectId, region, instanceId).CreateUserPayload(*payload).Execute, sqlserverflexUtils.RetryConfig) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) return @@ -405,18 +393,7 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r // Delete existing user // Workaround: The user deletion will be tried 5 times. In some cases the instance might be // in maintenance mode and the user API is temporarily unavailable. Usually this is only for 1-2 seconds. - config := utils.RetryConfig{ - Attempts: 5, - Backoff: func(attempt int) time.Duration { - // Wait for every attempt 5 seconds longer. 5s, 10s, 15s and so on - return time.Duration(attempt*5) * time.Second - }, - RetryStatusCodes: []int{ - http.StatusLocked, - http.StatusTooEarly, - }, - } - err = utils.RetryRequestWithoutResponse(ctx, r.client.DefaultAPI.DeleteUser(ctx, projectId, region, instanceId, userId).Execute, config) + err = utils.RetryRequestWithoutResponse(ctx, r.client.DefaultAPI.DeleteUser(ctx, projectId, region, instanceId, userId).Execute, sqlserverflexUtils.RetryConfig) if err != nil { if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound { return diff --git a/stackit/internal/services/sqlserverflex/utils/util.go b/stackit/internal/services/sqlserverflex/utils/util.go index f4b50fe52..07e48c1c1 100644 --- a/stackit/internal/services/sqlserverflex/utils/util.go +++ b/stackit/internal/services/sqlserverflex/utils/util.go @@ -3,6 +3,8 @@ package utils import ( "context" "fmt" + "net/http" + "time" sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v3api" @@ -29,3 +31,15 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags return apiClient } + +var RetryConfig = utils.RetryConfig{ + Attempts: 5, + Backoff: func(attempt int) time.Duration { + // Wait for every attempt 5 seconds longer. 5s, 10s, 15s and so on + return time.Duration(attempt*5) * time.Second + }, + RetryStatusCodes: []int{ + http.StatusLocked, + http.StatusTooEarly, + }, +}