From b6fe32106004a5cafe42f6fd66b9e1c3b4afb2d6 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Wed, 29 Apr 2026 10:58:46 +1000 Subject: [PATCH] Handle unknown dns_record content during config validation --- internal/provider/resource_dns_record.go | 19 ++++++- internal/provider/resource_dns_record_test.go | 54 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/internal/provider/resource_dns_record.go b/internal/provider/resource_dns_record.go index beb0583..760df03 100644 --- a/internal/provider/resource_dns_record.go +++ b/internal/provider/resource_dns_record.go @@ -110,12 +110,12 @@ func (r *dnsRecordResource) ValidateConfig(ctx context.Context, req resource.Val if skip { return } - content := stringPointerFromOptionalContent(config.Content) + content, contentKnown := stringPointerFromOptionalContentForValidation(config.Content) dynamicIntent, ok := resolveDynamicIntent(recordType, config.Content, config.Dynamic, &resp.Diagnostics) if !ok { return } - validateDNSRecordContentForType(recordType, content, dynamicIntent, &resp.Diagnostics) + validateDNSRecordContentForTypeWithKnowledge(recordType, content, contentKnown, dynamicIntent, &resp.Diagnostics) } func (r *dnsRecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { @@ -393,11 +393,22 @@ func stringPointerFromOptionalContent(value types.String) *string { return &trimmed } +func stringPointerFromOptionalContentForValidation(value types.String) (*string, bool) { + if value.IsUnknown() { + return nil, false + } + return stringPointerFromOptionalContent(value), true +} + func stringPointer(value string) *string { return &value } func validateDNSRecordContentForType(recordType string, content *string, dynamicIntent bool, diagnostics *diag.Diagnostics) bool { + return validateDNSRecordContentForTypeWithKnowledge(recordType, content, true, dynamicIntent, diagnostics) +} + +func validateDNSRecordContentForTypeWithKnowledge(recordType string, content *string, contentKnown bool, dynamicIntent bool, diagnostics *diag.Diagnostics) bool { normalizedType := strings.ToUpper(strings.TrimSpace(recordType)) trimmedContent := "" if content != nil { @@ -440,6 +451,10 @@ func validateDNSRecordContentForType(recordType string, content *string, dynamic return false } + if !contentKnown { + return true + } + if trimmedContent == "" { diagnostics.AddError( "Missing required content for DNS record type", diff --git a/internal/provider/resource_dns_record_test.go b/internal/provider/resource_dns_record_test.go index 040fda0..d58ba4c 100644 --- a/internal/provider/resource_dns_record_test.go +++ b/internal/provider/resource_dns_record_test.go @@ -1,6 +1,7 @@ package provider import ( + "strings" "testing" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -79,6 +80,59 @@ func TestResolveDynamicIntent(t *testing.T) { } } +func TestValidateDNSRecordContentForTypeWithKnowledge(t *testing.T) { + nonEmpty := "example.com" + blank := "" + + tests := []struct { + name string + recordType string + content *string + contentKnown bool + dynamic bool + wantValid bool + wantMessage string + }{ + {name: "CNAME unknown content is allowed during validate", recordType: "CNAME", content: nil, contentKnown: false, wantValid: true}, + {name: "CNAME null content errors", recordType: "CNAME", content: nil, contentKnown: true, wantValid: false, wantMessage: "Missing required content"}, + {name: "CNAME empty content errors", recordType: "CNAME", content: &blank, contentKnown: true, wantValid: false, wantMessage: "Missing required content"}, + {name: "CNAME known content passes", recordType: "CNAME", content: &nonEmpty, contentKnown: true, wantValid: true}, + {name: "CNAME dynamic true errors", recordType: "CNAME", content: nil, contentKnown: false, dynamic: true, wantValid: false, wantMessage: "Dynamic mode is only supported"}, + {name: "A dynamic omitted content passes", recordType: "A", content: nil, contentKnown: true, dynamic: true, wantValid: true}, + {name: "AAAA dynamic omitted content passes", recordType: "AAAA", content: nil, contentKnown: true, dynamic: true, wantValid: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + diags := diag.Diagnostics{} + got := validateDNSRecordContentForTypeWithKnowledge(tc.recordType, tc.content, tc.contentKnown, tc.dynamic, &diags) + if got != tc.wantValid { + t.Fatalf("validateDNSRecordContentForTypeWithKnowledge()=%v, want %v", got, tc.wantValid) + } + if tc.wantValid && diags.HasError() { + t.Fatalf("expected no diagnostics, got %#v", diags) + } + if !tc.wantValid { + if !diags.HasError() { + t.Fatal("expected diagnostics but got none") + } + if tc.wantMessage != "" && diags[0].Summary() != "" && !strings.Contains(diags[0].Summary(), tc.wantMessage) { + t.Fatalf("expected first diagnostic summary to contain %q, got %q", tc.wantMessage, diags[0].Summary()) + } + } + }) + } +} + +func TestStringPointerFromOptionalContentForValidation(t *testing.T) { + if content, known := stringPointerFromOptionalContentForValidation(types.StringUnknown()); known || content != nil { + t.Fatalf("expected unknown content to be unknown with nil pointer, got known=%v content=%v", known, content) + } + if content, known := stringPointerFromOptionalContentForValidation(types.StringNull()); !known || content != nil { + t.Fatalf("expected null content to be known with nil pointer, got known=%v content=%v", known, content) + } +} + func TestNormalizeRecordContentForState(t *testing.T) { if got := normalizeRecordContentForState("AAAA", "2001:0db8:0000:0000:0000:0000:0000:0123", false); got.ValueString() != "2001:db8::123" { t.Fatalf("expected canonical IPv6, got %q", got.ValueString())