From 4103c33fd2ae21bb35cc9696dcf70af60bf5b613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20H=C3=B6nle?= Date: Wed, 15 Oct 2025 11:16:30 +0200 Subject: [PATCH] fix(dns): store IDs immediately after provisioning (#1022) relates to STACKITTPR-373 --- .../services/dns/recordset/resource.go | 20 +++- .../internal/services/dns/zone/resource.go | 29 +++--- stackit/internal/utils/utils.go | 10 ++ stackit/internal/utils/utils_test.go | 92 +++++++++++++++++++ 4 files changed, 133 insertions(+), 18 deletions(-) diff --git a/stackit/internal/services/dns/recordset/resource.go b/stackit/internal/services/dns/recordset/resource.go index b00e67e6..9a904635 100644 --- a/stackit/internal/services/dns/recordset/resource.go +++ b/stackit/internal/services/dns/recordset/resource.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "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/booldefault" @@ -218,7 +217,16 @@ func (r *recordSetResource) Create(ctx context.Context, req resource.CreateReque core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Calling API: %v", err)) return } - ctx = tflog.SetField(ctx, "record_set_id", *recordSetResp.Rrset.Id) + + // 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{ + "project_id": projectId, + "zone_id": zoneId, + "record_set_id": *recordSetResp.Rrset.Id, + }) + if resp.Diagnostics.HasError() { + return + } waitResp, err := wait.CreateRecordSetWaitHandler(ctx, r.client, projectId, zoneId, *recordSetResp.Rrset.Id).WaitWithContext(ctx) if err != nil { @@ -372,9 +380,11 @@ func (r *recordSetResource) ImportState(ctx context.Context, req resource.Import return } - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("zone_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("record_set_id"), idParts[2])...) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ + "project_id": idParts[0], + "zone_id": idParts[1], + "record_set_id": idParts[2], + }) tflog.Info(ctx, "DNS record set state imported") } diff --git a/stackit/internal/services/dns/zone/resource.go b/stackit/internal/services/dns/zone/resource.go index 2b0645ff..4b05814c 100644 --- a/stackit/internal/services/dns/zone/resource.go +++ b/stackit/internal/services/dns/zone/resource.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "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/booldefault" @@ -280,8 +279,7 @@ func (r *zoneResource) Schema(_ context.Context, _ resource.SchemaRequest, resp func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) if resp.Diagnostics.HasError() { return } @@ -301,9 +299,17 @@ func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Calling API: %v", err)) return } - zoneId := *createResp.Zone.Id - ctx = tflog.SetField(ctx, "zone_id", zoneId) + // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler + zoneId := *createResp.Zone.Id + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ + "project_id": projectId, + "zone_id": zoneId, + }) + if resp.Diagnostics.HasError() { + return + } + waitResp, err := wait.CreateZoneWaitHandler(ctx, r.client, projectId, zoneId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Zone creation waiting: %v", err)) @@ -317,8 +323,7 @@ func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, r return } // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) if resp.Diagnostics.HasError() { return } @@ -451,13 +456,11 @@ func (r *zoneResource) ImportState(ctx context.Context, req resource.ImportState return } - projectId := idParts[0] - zoneId := idParts[1] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "zone_id", zoneId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ + "project_id": idParts[0], + "zone_id": idParts[1], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("zone_id"), zoneId)...) tflog.Info(ctx, "DNS zone state imported") } diff --git a/stackit/internal/utils/utils.go b/stackit/internal/utils/utils.go index 04b63b83..5190660f 100644 --- a/stackit/internal/utils/utils.go +++ b/stackit/internal/utils/utils.go @@ -7,6 +7,8 @@ import ( "regexp" "strings" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -186,3 +188,11 @@ func CheckListRemoval(ctx context.Context, configModelList, planModelList types. } } } + +// SetAndLogStateFields writes the given map of key-value pairs to the state +func SetAndLogStateFields(ctx context.Context, diags *diag.Diagnostics, state *tfsdk.State, values map[string]any) { + for key, val := range values { + ctx = tflog.SetField(ctx, key, val) + diags.Append(state.SetAttribute(ctx, path.Root(key), val)...) + } +} diff --git a/stackit/internal/utils/utils_test.go b/stackit/internal/utils/utils_test.go index da1d16f2..8d6851aa 100644 --- a/stackit/internal/utils/utils_test.go +++ b/stackit/internal/utils/utils_test.go @@ -6,6 +6,9 @@ import ( "reflect" "testing" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -551,3 +554,92 @@ func TestCheckListRemoval(t *testing.T) { }) } } + +func TestSetAndLogStateFields(t *testing.T) { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{}, + "instance_id": schema.StringAttribute{}, + }, + } + + type args struct { + diags *diag.Diagnostics + state *tfsdk.State + values map[string]interface{} + } + type want struct { + hasError bool + state *tfsdk.State + } + tests := []struct { + name string + args args + want want + }{ + { + name: "empty map", + args: args{ + diags: &diag.Diagnostics{}, + state: &tfsdk.State{}, + values: map[string]interface{}{}, + }, + want: want{ + hasError: false, + state: &tfsdk.State{}, + }, + }, + { + name: "base", + args: args{ + diags: &diag.Diagnostics{}, + state: func() *tfsdk.State { + ctx := context.Background() + state := tfsdk.State{ + Raw: tftypes.NewValue(testSchema.Type().TerraformType(ctx), map[string]tftypes.Value{ + "project_id": tftypes.NewValue(tftypes.String, "9b15d120-86f8-45f5-81d8-a554f09c7582"), + "instance_id": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + } + return &state + }(), + values: map[string]interface{}{ + "project_id": "a414f971-3f7a-4e9a-8671-51a8acb7bcc8", + "instance_id": "97073250-8cad-46c3-8424-6258ac0b3731", + }, + }, + want: want{ + hasError: false, + state: func() *tfsdk.State { + ctx := context.Background() + state := tfsdk.State{ + Raw: tftypes.NewValue(testSchema.Type().TerraformType(ctx), map[string]tftypes.Value{ + "project_id": tftypes.NewValue(tftypes.String, nil), + "instance_id": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + } + state.SetAttribute(ctx, path.Root("project_id"), "a414f971-3f7a-4e9a-8671-51a8acb7bcc8") + state.SetAttribute(ctx, path.Root("instance_id"), "97073250-8cad-46c3-8424-6258ac0b3731") + return &state + }(), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + SetAndLogStateFields(ctx, tt.args.diags, tt.args.state, tt.args.values) + + if tt.args.diags.HasError() != tt.want.hasError { + t.Errorf("TestSetAndLogStateFields() error count = %v, hasErr %v", tt.args.diags.ErrorsCount(), tt.want.hasError) + } + + diff := cmp.Diff(tt.args.state, tt.want.state) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +}