From e9583e8f2514ed5d30d64797603ae5ecf57f6097 Mon Sep 17 00:00:00 2001 From: Alexander Dahmen Date: Fri, 6 Mar 2026 13:25:59 +0100 Subject: [PATCH] fix(sqlserverflex): Store ids immediately after provisioning STACKITTPR-395 Signed-off-by: Alexander Dahmen --- .../sqlserverflex/instance/resource.go | 24 ++++- .../sqlserverflex/sqlserverflex_test.go | 102 ++++++++++++++++++ .../services/sqlserverflex/user/resource.go | 19 ++-- 3 files changed, 133 insertions(+), 12 deletions(-) create mode 100644 stackit/internal/services/sqlserverflex/sqlserverflex_test.go diff --git a/stackit/internal/services/sqlserverflex/instance/resource.go b/stackit/internal/services/sqlserverflex/instance/resource.go index 72a0de305..2ca1cb8f4 100644 --- a/stackit/internal/services/sqlserverflex/instance/resource.go +++ b/stackit/internal/services/sqlserverflex/instance/resource.go @@ -14,7 +14,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -407,8 +406,21 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques ctx = core.LogResponse(ctx) + if createResp.Id == nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", "Got empty instance id") + return + } + instanceId := *createResp.Id - ctx = tflog.SetField(ctx, "instance_id", instanceId) + // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": projectId, + "region": region, + "instance_id": instanceId, + }) + if resp.Diagnostics.HasError() { + return + } // The creation waiter sometimes returns an error from the API: "instance with id xxx has unexpected status Failure" // which can be avoided by sleeping before wait waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).SetSleepBeforeWait(30 * time.Second).WaitWithContext(ctx) @@ -653,9 +665,11 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS return } - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "instance_id": idParts[2], + }) tflog.Info(ctx, "SQLServer Flex instance state imported") } diff --git a/stackit/internal/services/sqlserverflex/sqlserverflex_test.go b/stackit/internal/services/sqlserverflex/sqlserverflex_test.go new file mode 100644 index 000000000..a35a417a9 --- /dev/null +++ b/stackit/internal/services/sqlserverflex/sqlserverflex_test.go @@ -0,0 +1,102 @@ +package sqlserverflex + +import ( + "fmt" + "net/http" + "regexp" + "testing" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +func TestSQLServerFlexInstanceSavesIDsOnError(t *testing.T) { + projectId := uuid.NewString() + instanceId := uuid.NewString() + const ( + name = "instance-name" + flavorCpu = 4 + flavorRam = 16 + flavorId = "4.16-Single" + region = "eu01" + ) + s := testutil.NewMockServer(t) + defer s.Server.Close() + tfConfig := fmt.Sprintf(` +provider "stackit" { + default_region = "%s" + sqlserverflex_custom_endpoint = "%s" + service_account_token = "mock-server-needs-no-auth" +} + +resource "stackit_sqlserverflex_instance" "instance" { + project_id = "%s" + name = "%s" + flavor = { + cpu = %d + ram = %d + } +} + +`, region, s.Server.URL, projectId, name, flavorCpu, flavorRam) + flavor := testutil.MockResponse{ + ToJsonBody: &sqlserverflex.ListFlavorsResponse{ + Flavors: &[]sqlserverflex.InstanceFlavorEntry{ + { + Cpu: utils.Ptr(int64(flavorCpu)), + Memory: utils.Ptr(int64(flavorRam)), + Id: utils.Ptr(flavorId), + Description: utils.Ptr("test-flavor-id"), + }, + }, + }, + } + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + PreConfig: func() { + s.Reset( + flavor, + testutil.MockResponse{ + Description: "create", + ToJsonBody: sqlserverflex.CreateInstanceResponse{ + Id: utils.Ptr(instanceId), + }, + }, + testutil.MockResponse{ + Description: "failing waiter", + StatusCode: http.StatusInternalServerError, + }, + ) + }, + Config: tfConfig, + ExpectError: regexp.MustCompile("Error creating instance.*"), + }, + { + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "refresh", + Handler: func(w http.ResponseWriter, req *http.Request) { + expected := fmt.Sprintf("/v2/projects/%s/regions/%s/instances/%s", projectId, region, instanceId) + if req.URL.Path != expected { + t.Errorf("expected request to %s, got %s", expected, req.URL.Path) + } + w.WriteHeader(http.StatusInternalServerError) + }, + }, + testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted}, + testutil.MockResponse{Description: "delete waiter", StatusCode: http.StatusNotFound}, + ) + }, + RefreshState: true, + ExpectError: regexp.MustCompile("Error reading instance*"), + }, + }, + }) +} diff --git a/stackit/internal/services/sqlserverflex/user/resource.go b/stackit/internal/services/sqlserverflex/user/resource.go index e73fb9b06..796dcfd2d 100644 --- a/stackit/internal/services/sqlserverflex/user/resource.go +++ b/stackit/internal/services/sqlserverflex/user/resource.go @@ -16,7 +16,6 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -255,8 +254,12 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r return } userId := *userResp.Item.Id - ctx = tflog.SetField(ctx, "user_id", userId) - + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": projectId, + "region": region, + "instance_id": instanceId, + "user_id": userId, + }) // Map response body to schema err = mapFieldsCreate(userResp, &model, region) if err != nil { @@ -372,10 +375,12 @@ func (r *userResource) ImportState(ctx context.Context, req resource.ImportState return } - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), idParts[3])...) + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "instance_id": idParts[2], + "user_id": idParts[3], + }) core.LogAndAddWarning(ctx, &resp.Diagnostics, "SQLServer Flex user imported with empty password", "The user password is not imported as it is only available upon creation of a new user. The password field will be empty.",