From 1f7fca23870727685af6b19a0e81f2e898f59d19 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Fri, 20 Mar 2026 17:49:32 +0100 Subject: [PATCH] fix: save ID to state after provisioning relates to STACKITTPR-584 --- .../services/edgecloud/instance/resource.go | 17 +- .../services/logs/instance/resource.go | 27 ++- stackit/internal/services/logs/logs_test.go | 82 ++++++++ .../services/mariadb/credential/resource.go | 19 +- .../services/mariadb/instance/resource.go | 21 +- .../internal/services/mariadb/mariadb_test.go | 179 ++++++++++++++++++ .../services/sfs/export-policy/resource.go | 11 +- .../services/sfs/resourcepool/resource.go | 12 +- stackit/internal/services/sfs/sfs_test.go | 164 ++++++++++++++++ .../internal/services/sfs/share/resource.go | 14 +- 10 files changed, 510 insertions(+), 36 deletions(-) create mode 100644 stackit/internal/services/logs/logs_test.go create mode 100644 stackit/internal/services/mariadb/mariadb_test.go create mode 100644 stackit/internal/services/sfs/sfs_test.go diff --git a/stackit/internal/services/edgecloud/instance/resource.go b/stackit/internal/services/edgecloud/instance/resource.go index 8a90cda18..10420606e 100644 --- a/stackit/internal/services/edgecloud/instance/resource.go +++ b/stackit/internal/services/edgecloud/instance/resource.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "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" @@ -281,12 +280,17 @@ func (i *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", "API returned nil Instance ID") return } + edgeCloudInstanceId := *createResp.Id - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + // 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, "instance_id": edgeCloudInstanceId, "region": region, }) + if resp.Diagnostics.HasError() { + return + } waitResp, err := edgewait.CreateOrUpdateInstanceWaitHandler(ctx, i.client, projectId, region, edgeCloudInstanceId).WaitWithContext(ctx) if err != nil { @@ -436,9 +440,12 @@ func (i *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, "Edge cloud Instance state imported") } // mapFields maps the API response to the Terraform model. diff --git a/stackit/internal/services/logs/instance/resource.go b/stackit/internal/services/logs/instance/resource.go index eb89b1f83..3204fde03 100644 --- a/stackit/internal/services/logs/instance/resource.go +++ b/stackit/internal/services/logs/instance/resource.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-framework/diag" - "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" @@ -18,7 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" - wait "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api/wait" + "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logs/utils" @@ -249,6 +248,21 @@ func (r *logsInstanceResource) Create(ctx context.Context, req resource.CreateRe ctx = core.LogResponse(ctx) + if createResp == nil || createResp.Id == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating Logs Instance", "Create API response: Incomplete response (id missing)") + return + } + instanceId := createResp.Id + // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler + ctx = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": projectId, + "region": region, + "instance_id": instanceId, + }) + if resp.Diagnostics.HasError() { + return + } + waitResp, err := wait.CreateLogsInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, regionId, createResp.Id).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating Logs Instance", fmt.Sprintf("Waiting for Logs Instance to become active: %v", err)) @@ -403,9 +417,12 @@ func (r *logsInstanceResource) ImportState(ctx context.Context, req resource.Imp core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing Logs Instance", fmt.Sprintf("Invalid import ID %q: expected format is `project_id`,`region`,`instance_id`", req.ID)) 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 = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "instance_id": idParts[2], + }) tflog.Info(ctx, "Logs Instance state imported") } diff --git a/stackit/internal/services/logs/logs_test.go b/stackit/internal/services/logs/logs_test.go new file mode 100644 index 000000000..2b5f01adb --- /dev/null +++ b/stackit/internal/services/logs/logs_test.go @@ -0,0 +1,82 @@ +package logs + +import ( + "fmt" + "net/http" + "regexp" + "testing" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +func TestLogsInstanceSavesIDsOnError(t *testing.T) { + projectId := uuid.NewString() + instanceId := uuid.NewString() + const ( + region = "eu01" + ) + s := testutil.NewMockServer(t) + defer s.Server.Close() + tfConfig := fmt.Sprintf(` +provider "stackit" { + default_region = "%s" + logs_custom_endpoint = "%s" + service_account_token = "mock-server-needs-no-auth" +} +resource "stackit_logs_instance" "logs" { + project_id = "%s" + display_name = "logs-instance-example" + retention_days = 30 +} +`, region, s.Server.URL, projectId) + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "create instance", + ToJsonBody: logs.LogsInstance{ + Id: instanceId, + }, + }, + testutil.MockResponse{ + Description: "failing waiter", + StatusCode: http.StatusInternalServerError, + }, + ) + }, + Config: tfConfig, + ExpectError: regexp.MustCompile("Error creating Logs Instance.*"), + }, + { + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "refresh", + Handler: func(w http.ResponseWriter, req *http.Request) { + expected := fmt.Sprintf("/v1/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 logs instance.*"), + }, + }, + }) +} diff --git a/stackit/internal/services/mariadb/credential/resource.go b/stackit/internal/services/mariadb/credential/resource.go index d106d9d4a..531ea9e0a 100644 --- a/stackit/internal/services/mariadb/credential/resource.go +++ b/stackit/internal/services/mariadb/credential/resource.go @@ -15,7 +15,6 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "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" @@ -190,7 +189,15 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ return } credentialId := *credentialsResp.Id - ctx = tflog.SetField(ctx, "credential_id", credentialId) + // 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, + "instance_id": instanceId, + "credential_id": credentialId, + }) + if resp.Diagnostics.HasError() { + return + } waitResp, err := wait.CreateCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) if err != nil { @@ -311,9 +318,11 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor return } - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credential_id"), idParts[2])...) + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "instance_id": idParts[1], + "credential_id": idParts[2], + }) tflog.Info(ctx, "MariaDB credential state imported") } diff --git a/stackit/internal/services/mariadb/instance/resource.go b/stackit/internal/services/mariadb/instance/resource.go index de563b789..351b4e11d 100644 --- a/stackit/internal/services/mariadb/instance/resource.go +++ b/stackit/internal/services/mariadb/instance/resource.go @@ -19,7 +19,6 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "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" @@ -319,7 +318,21 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques ctx = core.LogResponse(ctx) + if createResp == nil || createResp.InstanceId == nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", "Create API response: Incomplete response (id missing)") + return + } + instanceId := *createResp.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, + "instance_id": instanceId, + }) + if resp.Diagnostics.HasError() { + return + } + ctx = tflog.SetField(ctx, "instance_id", instanceId) waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) if err != nil { @@ -510,8 +523,10 @@ 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("instance_id"), idParts[1])...) + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "instance_id": idParts[1], + }) tflog.Info(ctx, "MariaDB instance state imported") } diff --git a/stackit/internal/services/mariadb/mariadb_test.go b/stackit/internal/services/mariadb/mariadb_test.go new file mode 100644 index 000000000..2fc23adcd --- /dev/null +++ b/stackit/internal/services/mariadb/mariadb_test.go @@ -0,0 +1,179 @@ +package mariadb + +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/mariadb" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +func TestMariaDBInstanceSavesIDsOnError(t *testing.T) { + projectId := uuid.NewString() + instanceId := uuid.NewString() + planId := uuid.NewString() + const ( + region = "eu01" + version = "10.11" + planName = "mariadb-plan" + ) + s := testutil.NewMockServer(t) + defer s.Server.Close() + tfConfig := fmt.Sprintf(` +provider "stackit" { + default_region = "%s" + mariadb_custom_endpoint = "%s" + service_account_token = "mock-server-needs-no-auth" + enable_beta_resources = true +} +resource "stackit_mariadb_instance" "example" { + project_id = "%s" + name = "example-instance" + version = "%s" + plan_name = "%s" + parameters = { + sgw_acl = "193.148.160.0/19,45.129.40.0/21,45.135.244.0/22" + } +} +`, region, s.Server.URL, projectId, version, planName) + + planList := testutil.MockResponse{ + Description: "plan instance", + ToJsonBody: mariadb.ListOfferingsResponse{ + Offerings: &[]mariadb.Offering{ + { + Plans: &[]mariadb.Plan{ + { + Id: utils.Ptr(planId), + Name: utils.Ptr(planName), + }, + }, + Version: utils.Ptr(version), + }, + }, + }, + } + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + PreConfig: func() { + s.Reset( + planList, + testutil.MockResponse{ + Description: "create instance", + ToJsonBody: mariadb.CreateInstanceResponse{ + InstanceId: 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("/v1/projects/%s/instances/%s", projectId, 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.StatusGone, + }, + ) + }, + RefreshState: true, + ExpectError: regexp.MustCompile("Error reading instance.*"), + }, + }, + }) +} + +func TestMariaDBCredentialsSavesIDsOnError(t *testing.T) { + projectId := uuid.NewString() + instanceId := uuid.NewString() + credentialId := uuid.NewString() + const ( + region = "eu01" + ) + s := testutil.NewMockServer(t) + defer s.Server.Close() + tfConfig := fmt.Sprintf(` +provider "stackit" { + default_region = "%s" + mariadb_custom_endpoint = "%s" + service_account_token = "mock-server-needs-no-auth" + enable_beta_resources = true +} +resource "stackit_mariadb_credential" "example" { + project_id = "%s" + instance_id = "%s" +} +`, region, s.Server.URL, projectId, instanceId) + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "create credentials", + ToJsonBody: mariadb.CredentialsResponse{ + Id: utils.Ptr(credentialId), + }, + }, + testutil.MockResponse{ + Description: "failing waiter", + StatusCode: http.StatusInternalServerError, + }, + ) + }, + Config: tfConfig, + ExpectError: regexp.MustCompile("Error creating credential.*"), + }, + { + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "refresh", + Handler: func(w http.ResponseWriter, req *http.Request) { + expected := fmt.Sprintf("/v1/projects/%s/instances/%s/credentials/%s", projectId, instanceId, credentialId) + 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 credential.*"), + }, + }, + }) +} diff --git a/stackit/internal/services/sfs/export-policy/resource.go b/stackit/internal/services/sfs/export-policy/resource.go index 01f6cc3d4..e9095a2ef 100644 --- a/stackit/internal/services/sfs/export-policy/resource.go +++ b/stackit/internal/services/sfs/export-policy/resource.go @@ -287,7 +287,7 @@ func (r *exportPolicyResource) Create(ctx context.Context, req resource.CreateRe return } // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ "project_id": projectId, "region": region, "policy_id": *createResp.ShareExportPolicy.Id, @@ -472,10 +472,11 @@ func (r *exportPolicyResource) ImportState(ctx context.Context, req resource.Imp 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("policy_id"), idParts[2])...) - + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "policy_id": idParts[2], + }) tflog.Info(ctx, "SFS export policy state import") } diff --git a/stackit/internal/services/sfs/resourcepool/resource.go b/stackit/internal/services/sfs/resourcepool/resource.go index 8e37a2157..a1fda38fb 100644 --- a/stackit/internal/services/sfs/resourcepool/resource.go +++ b/stackit/internal/services/sfs/resourcepool/resource.go @@ -10,7 +10,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" - "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/booldefault" @@ -245,7 +244,7 @@ func (r *resourcePoolResource) Create(ctx context.Context, req resource.CreateRe } // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ "project_id": projectId, "region": region, "resource_pool_id": *resourcePool.ResourcePool.Id, @@ -462,10 +461,11 @@ func (r *resourcePoolResource) ImportState(ctx context.Context, req resource.Imp 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("resource_pool_id"), idParts[2])...) - + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "resource_pool_id": idParts[2], + }) tflog.Info(ctx, "SFS resource pool imported") } diff --git a/stackit/internal/services/sfs/sfs_test.go b/stackit/internal/services/sfs/sfs_test.go new file mode 100644 index 000000000..a9af2fccf --- /dev/null +++ b/stackit/internal/services/sfs/sfs_test.go @@ -0,0 +1,164 @@ +package sfs + +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/sfs" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +func TestSfsResourcePoolSavesIDsOnError(t *testing.T) { + projectId := uuid.NewString() + instanceId := uuid.NewString() + const ( + region = "eu01" + ) + s := testutil.NewMockServer(t) + defer s.Server.Close() + tfConfig := fmt.Sprintf(` +provider "stackit" { + default_region = "%s" + sfs_custom_endpoint = "%s" + service_account_token = "mock-server-needs-no-auth" + enable_beta_resources = true +} +resource "stackit_sfs_resource_pool" "resourcepool" { + project_id = "%s" + name = "sfs-instance" + availability_zone = "eu01-m" + performance_class = "Standard" + size_gigabytes = 512 + ip_acl = ["192.168.2.0/24"] +} +`, region, s.Server.URL, projectId) + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "create instance", + ToJsonBody: sfs.CreateResourcePoolResponse{ + ResourcePool: &sfs.CreateResourcePoolResponseResourcePool{ + Id: utils.Ptr(instanceId), + }, + }, + }, + testutil.MockResponse{ + Description: "failing waiter", + StatusCode: http.StatusInternalServerError, + }, + ) + }, + Config: tfConfig, + ExpectError: regexp.MustCompile("Error creating resource pool.*"), + }, + { + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "refresh", + Handler: func(w http.ResponseWriter, req *http.Request) { + expected := fmt.Sprintf("/v1/projects/%s/regions/%s/resourcePools/%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 resource pool*"), + }, + }, + }) +} + +func TestSfsShareSavesIDsOnError(t *testing.T) { + projectId := uuid.NewString() + instanceId := uuid.NewString() + resourcePoolId := uuid.NewString() + const ( + region = "eu01" + ) + s := testutil.NewMockServer(t) + defer s.Server.Close() + tfConfig := fmt.Sprintf(` +provider "stackit" { + default_region = "%s" + sfs_custom_endpoint = "%s" + service_account_token = "mock-server-needs-no-auth" + enable_beta_resources = true +} +resource "stackit_sfs_share" "example" { + project_id = "%s" + resource_pool_id = "%s" + name = "my-nfs-share" + export_policy = "high-performance-class" + space_hard_limit_gigabytes = 32 +} +`, region, s.Server.URL, projectId, resourcePoolId) + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "create instance", + ToJsonBody: sfs.CreateShareResponse{ + Share: &sfs.CreateShareResponseShare{ + Id: utils.Ptr(instanceId), + }, + }, + }, + testutil.MockResponse{ + Description: "failing waiter", + StatusCode: http.StatusInternalServerError, + }, + ) + }, + Config: tfConfig, + ExpectError: regexp.MustCompile("Error creating share.*"), + }, + { + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "refresh", + Handler: func(w http.ResponseWriter, req *http.Request) { + expected := fmt.Sprintf("/v1/projects/%s/regions/%s/resourcePools/%s/shares/%s", projectId, region, resourcePoolId, 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 share*"), + }, + }, + }) +} diff --git a/stackit/internal/services/sfs/share/resource.go b/stackit/internal/services/sfs/share/resource.go index 56b021bd1..50d17e916 100644 --- a/stackit/internal/services/sfs/share/resource.go +++ b/stackit/internal/services/sfs/share/resource.go @@ -8,7 +8,6 @@ import ( "net/http" "strings" - "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" @@ -242,7 +241,7 @@ func (r *shareResource) Create(ctx context.Context, req resource.CreateRequest, return } // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ "project_id": projectId, "region": region, "resource_pool_id": resourcePoolId, @@ -466,11 +465,12 @@ func (r *shareResource) ImportState(ctx context.Context, req resource.ImportStat 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("resource_pool_id"), idParts[2])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("share_id"), idParts[3])...) - + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "resource_pool_id": idParts[2], + "share_id": idParts[3], + }) tflog.Info(ctx, "SFS share imported") }