From 58eb7fb52aa6c644413b4ce00cc7c5b96c4b438e Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Wed, 29 Apr 2026 12:33:18 +1000 Subject: [PATCH] Preserve unknown computed fields from state during dns update --- internal/provider/resource_dns_record.go | 54 ++++++++++-- .../resource_dns_record_integration_test.go | 87 ++++++++++++++++++- 2 files changed, 131 insertions(+), 10 deletions(-) diff --git a/internal/provider/resource_dns_record.go b/internal/provider/resource_dns_record.go index 9189211..b0790ff 100644 --- a/internal/provider/resource_dns_record.go +++ b/internal/provider/resource_dns_record.go @@ -198,18 +198,26 @@ func (r *dnsRecordResource) Read(ctx context.Context, req resource.ReadRequest, func (r *dnsRecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var plan dnsRecordResourceModel + var state dnsRecordResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } - domainID, recordID, err := parseDNSRecordID(plan.ID.ValueString()) + domainID, recordID, err := parseDNSRecordID(state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Invalid resource ID", err.Error()) return } - domainName := strings.TrimSpace(plan.DomainName.ValueString()) + domainName := "" + if !plan.DomainName.IsNull() && !plan.DomainName.IsUnknown() { + domainName = strings.TrimSpace(plan.DomainName.ValueString()) + } + if domainName == "" && !state.DomainName.IsNull() && !state.DomainName.IsUnknown() { + domainName = strings.TrimSpace(state.DomainName.ValueString()) + } if domainName == "" { _, resolvedDomainName, err := r.clientProvider.client.GetRootDomain(ctx, plan.Hostname.ValueString()) if err != nil { @@ -226,13 +234,13 @@ func (r *dnsRecordResource) Update(ctx context.Context, req resource.UpdateReque } updateReq := dynuclient.UpdateDNSRecordRequest{ - NodeName: recordNodeName(plan.NodeName, plan.Hostname, domainName), + NodeName: recordNodeName(preferKnownString(plan.NodeName, state.NodeName), plan.Hostname, domainName), RecordType: recordType, Content: stringPointerFromOptionalContent(plan.Content), - TTL: int64FromOptional(plan.TTL), - State: boolPointerFromOptional(plan.State), - Group: stringFromOptional(plan.Group), - Host: stringFromOptional(plan.Host), + TTL: int64FromOptional(preferKnownInt64(plan.TTL, state.TTL)), + State: boolPointerFromOptional(preferKnownBool(plan.State, state.State)), + Group: stringFromOptional(preferKnownString(plan.Group, state.Group)), + Host: stringFromOptional(preferKnownString(plan.Host, state.Host)), } if !validateDNSRecordContentForType(updateReq.RecordType, updateReq.Content, dynamicIntent, &resp.Diagnostics) { return @@ -267,7 +275,7 @@ func (r *dnsRecordResource) Update(ctx context.Context, req resource.UpdateReque } nextState := mapDNSRecordToState(*record, dynamicIntent) - nextState.ID = types.StringValue(formatDNSRecordID(record.DomainID, record.ID)) + nextState.ID = state.ID resp.Diagnostics.Append(resp.State.Set(ctx, &nextState)...) } @@ -382,6 +390,36 @@ func stringFromOptional(value types.String) string { return strings.TrimSpace(value.ValueString()) } +func preferKnownString(planValue types.String, stateValue types.String) types.String { + if !planValue.IsNull() && !planValue.IsUnknown() { + return planValue + } + if !stateValue.IsNull() && !stateValue.IsUnknown() { + return stateValue + } + return planValue +} + +func preferKnownInt64(planValue types.Int64, stateValue types.Int64) types.Int64 { + if !planValue.IsNull() && !planValue.IsUnknown() { + return planValue + } + if !stateValue.IsNull() && !stateValue.IsUnknown() { + return stateValue + } + return planValue +} + +func preferKnownBool(planValue types.Bool, stateValue types.Bool) types.Bool { + if !planValue.IsNull() && !planValue.IsUnknown() { + return planValue + } + if !stateValue.IsNull() && !stateValue.IsUnknown() { + return stateValue + } + return planValue +} + func stringPointerFromOptionalContent(value types.String) *string { if value.IsNull() || value.IsUnknown() { return nil diff --git a/internal/provider/resource_dns_record_integration_test.go b/internal/provider/resource_dns_record_integration_test.go index edacb9f..7cf7efb 100644 --- a/internal/provider/resource_dns_record_integration_test.go +++ b/internal/provider/resource_dns_record_integration_test.go @@ -64,7 +64,7 @@ func TestIntegrationResourceDNSRecordLifecycleAndImport(t *testing.T) { } updateResp := resource.UpdateResponse{State: tfsdk.State{Schema: schemaResp.Schema}} - r.Update(ctx, resource.UpdateRequest{Plan: plan}, &updateResp) + r.Update(ctx, resource.UpdateRequest{Plan: plan, State: createResp.State}, &updateResp) if updateResp.Diagnostics.HasError() { t.Fatalf("update diagnostics: %v", updateResp.Diagnostics) } @@ -204,7 +204,7 @@ func TestIntegrationResourceDNSRecordDynamicAStateStableAndTransitionToStatic(t } updateResp := resource.UpdateResponse{State: tfsdk.State{Schema: schemaResp.Schema}} - r.Update(ctx, resource.UpdateRequest{Plan: plan}, &updateResp) + r.Update(ctx, resource.UpdateRequest{Plan: plan, State: createResp.State}, &updateResp) if updateResp.Diagnostics.HasError() { t.Fatalf("update diagnostics: %v", updateResp.Diagnostics) } @@ -219,6 +219,89 @@ func TestIntegrationResourceDNSRecordDynamicAStateStableAndTransitionToStatic(t } } +func TestIntegrationResourceDNSRecordUpdateUsesStateIDWhenPlanIDUnknown(t *testing.T) { + ctx := context.Background() + fake := fakedynu.NewServer() + defer fake.Close() + + r := NewDNSRecordResource().(*dnsRecordResource) + configureResource(t, r, fake.BaseURL()) + + var schemaResp resource.SchemaResponse + r.Schema(ctx, resource.SchemaRequest{}, &schemaResp) + + createPlan := dnsRecordResourceModel{ + Hostname: types.StringValue("api.a.example.com"), + RecordType: types.StringValue("TXT"), + Content: types.StringValue("v=one"), + TTL: types.Int64Value(60), + State: types.BoolValue(true), + Group: types.StringValue("test-group"), + Host: types.StringValue("test-host"), + NodeName: types.StringValue("test-node"), + } + + plan := tfsdk.Plan{Schema: schemaResp.Schema} + if diags := plan.Set(ctx, &createPlan); diags.HasError() { + t.Fatalf("set create plan diagnostics: %v", diags) + } + + createResp := resource.CreateResponse{State: tfsdk.State{Schema: schemaResp.Schema}} + r.Create(ctx, resource.CreateRequest{Plan: plan}, &createResp) + if createResp.Diagnostics.HasError() { + t.Fatalf("create diagnostics: %v", createResp.Diagnostics) + } + + var state dnsRecordResourceModel + if diags := createResp.State.Get(ctx, &state); diags.HasError() { + t.Fatalf("state get diagnostics: %v", diags) + } + + updatePlan := state + updatePlan.ID = types.StringUnknown() + updatePlan.DomainID = types.Int64Unknown() + updatePlan.DomainName = types.StringUnknown() + updatePlan.Dynamic = types.BoolUnknown() + updatePlan.Group = types.StringUnknown() + updatePlan.Host = types.StringUnknown() + updatePlan.NodeName = types.StringUnknown() + updatePlan.UpdatedOn = types.StringUnknown() + updatePlan.Content = types.StringValue("v=two") + + plan = tfsdk.Plan{Schema: schemaResp.Schema} + if diags := plan.Set(ctx, &updatePlan); diags.HasError() { + t.Fatalf("set update plan diagnostics: %v", diags) + } + + updateResp := resource.UpdateResponse{State: tfsdk.State{Schema: schemaResp.Schema}} + r.Update(ctx, resource.UpdateRequest{ + Plan: plan, + State: createResp.State, + }, &updateResp) + if updateResp.Diagnostics.HasError() { + t.Fatalf("update diagnostics: %v", updateResp.Diagnostics) + } + + if diags := updateResp.State.Get(ctx, &state); diags.HasError() { + t.Fatalf("updated state diagnostics: %v", diags) + } + if state.ID.IsNull() || state.ID.IsUnknown() { + t.Fatalf("expected ID to remain known after update") + } + if state.Content.ValueString() != "v=two" { + t.Fatalf("expected updated content, got %q", state.Content.ValueString()) + } + if state.Group.ValueString() != "test-group" { + t.Fatalf("expected group preserved from state, got %q", state.Group.ValueString()) + } + if state.Host.ValueString() != "test-host" { + t.Fatalf("expected host preserved from state, got %q", state.Host.ValueString()) + } + if state.NodeName.ValueString() != "test-node" { + t.Fatalf("expected node_name preserved from state, got %q", state.NodeName.ValueString()) + } +} + func configureResource(t *testing.T, r resource.ResourceWithConfigure, baseURL string) { t.Helper() resp := resource.ConfigureResponse{}