From 2a1b047de627b40a5a9b2f12b2812a9a9da72555 Mon Sep 17 00:00:00 2001 From: "Marcel S. Henselin" Date: Thu, 12 Feb 2026 12:41:11 +0100 Subject: [PATCH] fix: sqlserver beta fixes --- .../sqlserverflexalpha/database/resource.go | 4 +- .../sqlserverflexbeta/database/mapper.go | 3 + .../sqlserverflexbeta/database/resource.go | 42 ++++- .../sqlserverflexbeta/instance/resource.go | 2 +- .../sqlserverflex_acc_test.go | 9 +- .../testdata/instance_template.gompl | 11 +- .../services/sqlserverflexbeta/user/mapper.go | 3 + .../sqlserverflexbeta/user/resource.go | 84 ++++++++- .../internal/wait/sqlserverflexbeta/wait.go | 168 ++++++++++++++++-- 9 files changed, 291 insertions(+), 35 deletions(-) diff --git a/stackit/internal/services/sqlserverflexalpha/database/resource.go b/stackit/internal/services/sqlserverflexalpha/database/resource.go index f95b57fb..0ffba164 100644 --- a/stackit/internal/services/sqlserverflexalpha/database/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/database/resource.go @@ -269,7 +269,7 @@ func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, r ctx = core.InitProviderContext(ctx) - projectId, instanceId, region, databaseName, errExt := r.extractIdentityData(model, identityData) + projectId, region, instanceId, databaseName, errExt := r.extractIdentityData(model, identityData) if errExt != nil { core.LogAndAddError( ctx, @@ -353,7 +353,7 @@ func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteReques ctx = core.InitProviderContext(ctx) - projectId, instanceId, region, databaseName, errExt := r.extractIdentityData(model, identityData) + projectId, region, instanceId, databaseName, errExt := r.extractIdentityData(model, identityData) if errExt != nil { core.LogAndAddError( ctx, diff --git a/stackit/internal/services/sqlserverflexbeta/database/mapper.go b/stackit/internal/services/sqlserverflexbeta/database/mapper.go index 51772af1..349f7717 100644 --- a/stackit/internal/services/sqlserverflexbeta/database/mapper.go +++ b/stackit/internal/services/sqlserverflexbeta/database/mapper.go @@ -79,6 +79,9 @@ func mapResourceFields(source *sqlserverflexbeta.GetDatabaseResponse, model *res model.ProjectId = types.StringValue(model.ProjectId.ValueString()) model.InstanceId = types.StringValue(model.InstanceId.ValueString()) + model.CompatibilityLevel = types.Int64Value(source.GetCompatibilityLevel()) + model.CollationName = types.StringValue(source.GetCollationName()) + return nil } diff --git a/stackit/internal/services/sqlserverflexbeta/database/resource.go b/stackit/internal/services/sqlserverflexbeta/database/resource.go index 1f57355d..652575fc 100644 --- a/stackit/internal/services/sqlserverflexbeta/database/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/database/resource.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -17,6 +18,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" + wait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/sqlserverflexbeta" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" @@ -59,7 +61,7 @@ type DatabaseResourceIdentityModel struct { } func (r *databaseResource) Metadata( - ctx context.Context, + _ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse, ) { @@ -179,6 +181,26 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques Owner: data.Owner.ValueStringPointer(), } + _, err := wait.WaitForUserWaitHandler( + ctx, + r.client, + projectId, + instanceId, + region, + data.Owner.ValueString(), + ). + SetSleepBeforeWait(10 * time.Second). + WaitWithContext(ctx) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + createErr, + fmt.Sprintf("Calling API: %v", err), + ) + return + } + createResp, err := r.client.CreateDatabaseRequest(ctx, projectId, region, instanceId). CreateDatabaseRequestPayload(payLoad). Execute() @@ -221,7 +243,7 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques } // TODO: is this neccessary to wait for the database-> API say 200 ? - /*waitResp, err := wait.CreateDatabaseWaitHandler( + waitResp, err := wait.CreateDatabaseWaitHandler( ctx, r.client, projectId, @@ -253,7 +275,7 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques return } - if *waitResp.Id != createId { + if *waitResp.Id != databaseId { core.LogAndAddError( ctx, &resp.Diagnostics, @@ -281,7 +303,7 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques "Database creation waiting: returned name is different", ) return - }*/ + } database, err := r.client.GetDatabaseRequest(ctx, projectId, region, instanceId, databaseName).Execute() if err != nil { @@ -334,7 +356,7 @@ func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, r ctx = core.InitProviderContext(ctx) - projectId, instanceId, region, databaseName, errExt := r.extractIdentityData(model, identityData) + projectId, region, instanceId, databaseName, errExt := r.extractIdentityData(model, identityData) if errExt != nil { core.LogAndAddError( ctx, @@ -418,7 +440,7 @@ func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteReques ctx = core.InitProviderContext(ctx) - projectId, instanceId, region, databaseName, errExt := r.extractIdentityData(model, identityData) + projectId, region, instanceId, databaseName, errExt := r.extractIdentityData(model, identityData) if errExt != nil { core.LogAndAddError( ctx, @@ -436,7 +458,13 @@ func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteReques // Delete existing record set err := r.client.DeleteDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseName) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting database", fmt.Sprintf("Calling API: %v", err)) + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error deleting database", + fmt.Sprintf( + "Calling API: %v\nname: %s, region: %s, instanceId: %s", err, databaseName, region, instanceId)) + return } ctx = core.LogResponse(ctx) diff --git a/stackit/internal/services/sqlserverflexbeta/instance/resource.go b/stackit/internal/services/sqlserverflexbeta/instance/resource.go index 741424a0..6abcbbd3 100644 --- a/stackit/internal/services/sqlserverflexbeta/instance/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/instance/resource.go @@ -241,7 +241,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques InstanceId, region, ).SetSleepBeforeWait( - 30 * time.Second, + 10 * time.Second, ).SetTimeout( 90 * time.Minute, ).WaitWithContext(ctx) diff --git a/stackit/internal/services/sqlserverflexbeta/sqlserverflex_acc_test.go b/stackit/internal/services/sqlserverflexbeta/sqlserverflex_acc_test.go index 9a3bb1b7..9c091cde 100644 --- a/stackit/internal/services/sqlserverflexbeta/sqlserverflex_acc_test.go +++ b/stackit/internal/services/sqlserverflexbeta/sqlserverflex_acc_test.go @@ -215,6 +215,7 @@ func TestAccInstanceWithUsers(t *testing.T) { func TestAccInstanceWithDatabases(t *testing.T) { data := getExample() + t.Logf(" ... working on instance %s", data.TfName) dbName := "testDb" userName := "testUser" data.Users = []User{ @@ -258,13 +259,15 @@ func TestAccInstanceWithDatabases(t *testing.T) { data, ), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(resName, "name", data.Name), resource.TestCheckResourceAttrSet(resName, "id"), - resource.TestCheckResourceAttr(resUserName, "name", userName), + resource.TestCheckResourceAttr(resName, "name", data.Name), + resource.TestCheckResourceAttrSet(resUserName, "id"), + resource.TestCheckResourceAttr(resUserName, "username", userName), + + resource.TestCheckResourceAttrSet(resDbName, "id"), resource.TestCheckResourceAttr(resDbName, "name", dbName), resource.TestCheckResourceAttr(resDbName, "owner", userName), - resource.TestCheckResourceAttrSet(resDbName, "id"), ), }, }, diff --git a/stackit/internal/services/sqlserverflexbeta/testdata/instance_template.gompl b/stackit/internal/services/sqlserverflexbeta/testdata/instance_template.gompl index ddc95138..84ca19c1 100644 --- a/stackit/internal/services/sqlserverflexbeta/testdata/instance_template.gompl +++ b/stackit/internal/services/sqlserverflexbeta/testdata/instance_template.gompl @@ -44,10 +44,13 @@ resource "stackitprivatepreview_sqlserverflexbeta_user" "{{ $user.Name }}" { {{ $tfName := .TfName }} {{ range $db := .Databases }} resource "stackitprivatepreview_sqlserverflexbeta_database" "{{ $db.Name }}" { - project_id = "{{ $db.ProjectId }}" - instance_id = stackitprivatepreview_sqlserverflexbeta_instance.{{ $tfName }}.instance_id - name = "{{ $db.Name }}" - owner = "{{ $db.Owner }}" + depends_on = [stackitprivatepreview_sqlserverflexbeta_user.{{ $db.Owner }}] + project_id = "{{ $db.ProjectId }}" + instance_id = stackitprivatepreview_sqlserverflexbeta_instance.{{ $tfName }}.instance_id + name = "{{ $db.Name }}" + owner = "{{ $db.Owner }}" + collation = "Albanian_BIN" + compatibility = "160" } {{ end }} {{ end }} diff --git a/stackit/internal/services/sqlserverflexbeta/user/mapper.go b/stackit/internal/services/sqlserverflexbeta/user/mapper.go index bddea43a..59c915bb 100644 --- a/stackit/internal/services/sqlserverflexbeta/user/mapper.go +++ b/stackit/internal/services/sqlserverflexbeta/user/mapper.go @@ -153,6 +153,9 @@ func mapFieldsCreate(userResp *sqlserverflexbeta.CreateUserResponse, model *reso model.Roles = types.List(types.SetNull(types.StringType)) } + model.Password = types.StringPointerValue(user.Password) + model.Uri = types.StringPointerValue(user.Uri) + model.Host = types.StringPointerValue(user.Host) model.Port = types.Int64PointerValue(user.Port) model.Region = types.StringValue(region) diff --git a/stackit/internal/services/sqlserverflexbeta/user/resource.go b/stackit/internal/services/sqlserverflexbeta/user/resource.go index 49850497..a31c380b 100644 --- a/stackit/internal/services/sqlserverflexbeta/user/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/user/resource.go @@ -8,6 +8,7 @@ import ( "net/http" "strconv" "strings" + "time" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -47,7 +48,7 @@ type UserResourceIdentityModel struct { ProjectID types.String `tfsdk:"project_id"` Region types.String `tfsdk:"region"` InstanceID types.String `tfsdk:"instance_id"` - UserID types.Int64 `tfsdk:"database_id"` + UserID types.Int64 `tfsdk:"user_id"` } type userResource struct { @@ -215,10 +216,22 @@ func (r *userResource) Create( ) return } + userId := *userResp.Id ctx = tflog.SetField(ctx, "user_id", userId) - // Map response body to schema + // Set data returned by API in identity + identity := UserResourceIdentityModel{ + ProjectID: types.StringValue(projectId), + Region: types.StringValue(region), + InstanceID: types.StringValue(instanceId), + UserID: types.Int64Value(userId), + } + resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) + if resp.Diagnostics.HasError() { + return + } + err = mapFieldsCreate(userResp, &model, region) if err != nil { core.LogAndAddError( @@ -229,6 +242,51 @@ func (r *userResource) Create( ) return } + + waitResp, err := sqlserverflexbetaWait.CreateUserWaitHandler( + ctx, + r.client, + projectId, + instanceId, + region, + userId, + ).SetSleepBeforeWait( + 90 * time.Second, + ).SetTimeout( + 90 * time.Minute, + ).WaitWithContext(ctx) + + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "create user", + fmt.Sprintf("Instance creation waiting: %v", err), + ) + return + } + + if waitResp.Id == nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "create user", + "Instance creation waiting: returned id is nil", + ) + return + } + + // Map response body to schema + err = mapFields(waitResp, &model, region) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error creating user", + fmt.Sprintf("Processing API payload: %v", err), + ) + return + } // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) @@ -350,9 +408,29 @@ func (r *userResource) Delete( // Delete existing record set // err := r.client.DeleteUserRequest(ctx, projectId, region, instanceId, userId).Execute() + err := r.client.DeleteUserRequestExecute(ctx, projectId, region, instanceId, userId) + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if !ok { + // TODO err handling + return + } + switch oapiErr.StatusCode { + case http.StatusNotFound: + resp.State.RemoveResource(ctx) + return + // case http.StatusInternalServerError: + // tflog.Warn(ctx, "[delete user] Wait handler got error 500") + // return false, nil, nil + default: + // TODO err handling + return + } + } // Delete existing record set - _, err := sqlserverflexbetaWait.DeleteUserWaitHandler(ctx, r.client, projectId, region, instanceId, userId). + _, err = sqlserverflexbetaWait.DeleteUserWaitHandler(ctx, r.client, projectId, region, instanceId, userId). WaitWithContext(ctx) //err := r.client.DeleteUserRequest(ctx, arg.projectId, arg.region, arg.instanceId, userId).Execute() if err != nil { diff --git a/stackit/internal/wait/sqlserverflexbeta/wait.go b/stackit/internal/wait/sqlserverflexbeta/wait.go index 3bef0c64..b6a5bfcd 100644 --- a/stackit/internal/wait/sqlserverflexbeta/wait.go +++ b/stackit/internal/wait/sqlserverflexbeta/wait.go @@ -45,6 +45,22 @@ type APIClientInterface interface { instanceId string, userId int64, ) (*sqlserverflex.GetUserResponse, error) + + ListRolesRequestExecute( + ctx context.Context, + projectId string, + region string, + instanceId string, + ) (*sqlserverflex.ListRolesResponse, error) + + ListUsersRequest(ctx context.Context, projectId string, region string, instanceId string) sqlserverflex.ApiListUsersRequestRequest + + ListUsersRequestExecute( + ctx context.Context, + projectId string, + region string, + instanceId string, + ) (*sqlserverflex.ListUserResponse, error) } // APIClientUserInterface Interface needed for tests @@ -85,6 +101,54 @@ func CreateInstanceWaitHandler( return false, nil, nil } } + + tflog.Info(ctx, "trying to get roles") + time.Sleep(10 * time.Second) + _, rolesErr := a.ListRolesRequestExecute(ctx, projectId, region, instanceId) + if rolesErr != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(rolesErr, &oapiErr) + if !ok { + return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError") + } + if oapiErr.StatusCode != http.StatusInternalServerError { + tflog.Info( + ctx, "got error from api", map[string]interface{}{ + "error": rolesErr.Error(), + }, + ) + return false, nil, rolesErr + } + tflog.Info( + ctx, "wait for get-roles to work hack", map[string]interface{}{}, + ) + time.Sleep(10 * time.Second) + return false, nil, nil + } + + tflog.Info(ctx, "trying to get users") + time.Sleep(10 * time.Second) + _, usersErr := a.ListUsersRequestExecute(ctx, projectId, region, instanceId) + if usersErr != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(usersErr, &oapiErr) + if !ok { + return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError") + } + if oapiErr.StatusCode != http.StatusInternalServerError { + tflog.Info( + ctx, "got error from api", map[string]interface{}{ + "error": rolesErr.Error(), + }, + ) + return false, nil, usersErr + } + tflog.Info( + ctx, "wait for get-users to work hack", map[string]interface{}{}, + ) + time.Sleep(10 * time.Second) + return false, nil, nil + } return true, s, nil case strings.ToLower(InstanceStateUnknown), strings.ToLower(InstanceStateFailed): return true, s, fmt.Errorf("create failed for instance with id %s", instanceId) @@ -94,6 +158,7 @@ func CreateInstanceWaitHandler( "status": *s.Status, }, ) + time.Sleep(10 * time.Second) return false, nil, nil default: tflog.Info( @@ -187,20 +252,96 @@ func CreateDatabaseWaitHandler( func() (waitFinished bool, response *sqlserverflex.GetDatabaseResponse, err error) { s, err := a.GetDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseName) if err != nil { - return false, nil, err - } - if s == nil || s.Name == nil || *s.Name != databaseName { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if !ok { + return false, nil, fmt.Errorf("get database - could not convert error to oapierror.GenericOpenAPIError: %s", err.Error()) + } + if oapiErr.StatusCode != http.StatusNotFound { + return false, nil, err + } return false, nil, nil } - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) + if s == nil || s.Name == nil || *s.Name != databaseName { + return false, nil, errors.New("response did return different result") + } + return true, s, nil + }, + ) + return handler +} + +// CreateUserWaitHandler will wait for instance creation +func CreateUserWaitHandler( + ctx context.Context, + a APIClientInterface, + projectId, instanceId, region string, + userId int64, +) *wait.AsyncActionHandler[sqlserverflex.GetUserResponse] { + handler := wait.New( + func() (waitFinished bool, response *sqlserverflex.GetUserResponse, err error) { + s, err := a.GetUserRequestExecute(ctx, projectId, region, instanceId, userId) + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if !ok { + return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError") + } + if oapiErr.StatusCode != http.StatusNotFound { + return false, nil, err + } + return false, nil, nil + } + return true, s, nil + }, + ) + return handler +} + +// WaitForUserWaitHandler will wait for instance creation +func WaitForUserWaitHandler( + ctx context.Context, + a APIClientInterface, + projectId, instanceId, region, userName string, +) *wait.AsyncActionHandler[sqlserverflex.ListUserResponse] { + startTime := time.Now() + timeOut := 2 * time.Minute + + handler := wait.New( + func() (waitFinished bool, response *sqlserverflex.ListUserResponse, err error) { + if time.Since(startTime) > timeOut { + return false, nil, errors.New("ran into timeout") + } + s, err := a.ListUsersRequest(ctx, projectId, region, instanceId).Size(100).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if !ok { + return false, nil, fmt.Errorf("Wait (list users) could not convert error to oapierror.GenericOpenAPIError: %s", err.Error()) + } + if oapiErr.StatusCode != http.StatusNotFound { + return false, nil, err + } + tflog.Info( + ctx, "Wait (list users) still waiting", map[string]interface{}{}, + ) + + return false, nil, nil + } + users, ok := s.GetUsersOk() if !ok { - return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError") + return false, nil, errors.New("no users found") } - if oapiErr.StatusCode != http.StatusNotFound { - return false, nil, err + + for _, u := range users { + if u.GetUsername() == userName { + return true, s, nil + } } - return true, nil, nil + tflog.Info( + ctx, "Wait (list users) user still not present", map[string]interface{}{}, + ) + return false, nil, nil }, ) return handler @@ -209,13 +350,13 @@ func CreateDatabaseWaitHandler( // DeleteUserWaitHandler will wait for instance deletion func DeleteUserWaitHandler( ctx context.Context, - a APIClientUserInterface, - projectId, instanceId, region string, + a APIClientInterface, + projectId, region, instanceId string, userId int64, ) *wait.AsyncActionHandler[struct{}] { handler := wait.New( func() (waitFinished bool, response *struct{}, err error) { - err = a.DeleteUserRequestExecute(ctx, projectId, region, instanceId, userId) + _, err = a.GetUserRequestExecute(ctx, projectId, region, instanceId, userId) if err == nil { return false, nil, nil } @@ -228,9 +369,6 @@ func DeleteUserWaitHandler( switch oapiErr.StatusCode { case http.StatusNotFound: return true, nil, nil - case http.StatusInternalServerError: - tflog.Warn(ctx, "Wait handler got error 500") - return false, nil, nil default: return false, nil, err } -- 2.49.1