diff --git a/internal/dynuclient/client.go b/internal/dynuclient/client.go index fcc4eb8..5a67c61 100644 --- a/internal/dynuclient/client.go +++ b/internal/dynuclient/client.go @@ -343,7 +343,7 @@ func buildDNSRecordUpsertPayload(recordType string, nodeName string, content *st payload.IPv6Address = *normalizedContent } case "CNAME": - if payload.Host == "" && normalizedContent != nil { + if normalizedContent != nil { payload.Host = *normalizedContent } default: diff --git a/internal/dynuclient/client_test.go b/internal/dynuclient/client_test.go index e64bac0..71e8930 100644 --- a/internal/dynuclient/client_test.go +++ b/internal/dynuclient/client_test.go @@ -290,6 +290,34 @@ func TestClientCreateDNSRecordSendsHostForCNAMERecord(t *testing.T) { } } +func TestClientUpdateDNSRecordCNAMEContentOverridesHost(t *testing.T) { + var captured map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/dns/1001/record/11" { + t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) + } + if err := json.NewDecoder(r.Body).Decode(&captured); err != nil { + t.Fatalf("failed to decode payload: %v", err) + } + _ = json.NewEncoder(w).Encode(map[string]any{"statusCode": 200, "id": 11, "domainId": 1001, "recordType": "CNAME", "host": "example.co"}) + })) + defer server.Close() + + client := dynuclient.New("test-key", dynuclient.WithBaseURL(server.URL), dynuclient.WithHTTPClient(server.Client())) + _, err := client.UpdateDNSRecord(context.Background(), 1001, 11, dynuclient.UpdateDNSRecordRequest{ + NodeName: "alias", + RecordType: "CNAME", + Content: stringPointer("example.co"), + Host: "example.com", + }) + if err != nil { + t.Fatalf("UpdateDNSRecord() error = %v", err) + } + if captured["host"] != "example.co" { + t.Fatalf("expected host to be overridden by content, got %#v", captured["host"]) + } +} + func TestClientCreateDNSRecordSendsContentForTXTRecord(t *testing.T) { var captured map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/provider/resource_dns_record.go b/internal/provider/resource_dns_record.go index 1c954fe..db0c547 100644 --- a/internal/provider/resource_dns_record.go +++ b/internal/provider/resource_dns_record.go @@ -146,6 +146,7 @@ func (r *dnsRecordResource) Create(ctx context.Context, req resource.CreateReque Group: stringFromOptional(plan.Group), Host: stringFromOptional(plan.Host), } + createReq = normalizeDNSRecordCreateRequestForType(createReq) if !validateDNSRecordContentForType(createReq.RecordType, createReq.Content, dynamicIntent, &resp.Diagnostics) { return } @@ -242,6 +243,7 @@ func (r *dnsRecordResource) Update(ctx context.Context, req resource.UpdateReque Group: stringFromOptional(preferKnownString(plan.Group, state.Group)), Host: stringFromOptional(preferKnownString(plan.Host, state.Host)), } + updateReq = normalizeDNSRecordUpdateRequestForType(updateReq) if !validateDNSRecordContentForType(updateReq.RecordType, updateReq.Content, dynamicIntent, &resp.Diagnostics) { return } @@ -315,7 +317,7 @@ func (r *dnsRecordResource) ImportState(ctx context.Context, req resource.Import } func mapDNSRecordToState(record dynuclient.DNSRecord, dynamicIntent bool) dnsRecordResourceModel { - content := normalizeRecordContentForState(record.RecordType, record.Content, dynamicIntent) + content := normalizeRecordContentForState(record.RecordType, record.Content, dynamicIntent, record.Host) return dnsRecordResourceModel{ Hostname: mapString(record.Hostname), RecordType: mapString(record.RecordType), @@ -442,6 +444,35 @@ func stringPointer(value string) *string { return &value } +func normalizeDNSRecordCreateRequestForType(req dynuclient.CreateDNSRecordRequest) dynuclient.CreateDNSRecordRequest { + if strings.EqualFold(strings.TrimSpace(req.RecordType), "CNAME") { + if normalizedContent := normalizeOptionalContentString(req.Content); normalizedContent != nil { + req.Host = *normalizedContent + } + } + return req +} + +func normalizeDNSRecordUpdateRequestForType(req dynuclient.UpdateDNSRecordRequest) dynuclient.UpdateDNSRecordRequest { + if strings.EqualFold(strings.TrimSpace(req.RecordType), "CNAME") { + if normalizedContent := normalizeOptionalContentString(req.Content); normalizedContent != nil { + req.Host = *normalizedContent + } + } + return req +} + +func normalizeOptionalContentString(content *string) *string { + if content == nil { + return nil + } + trimmed := strings.TrimSpace(*content) + if trimmed == "" { + return nil + } + return &trimmed +} + func validateDNSRecordContentForType(recordType string, content *string, dynamicIntent bool, diagnostics *diag.Diagnostics) bool { return validateDNSRecordContentForTypeWithKnowledge(recordType, content, true, dynamicIntent, diagnostics) } @@ -548,7 +579,7 @@ func inferDynamicIntentFromState(recordType types.String, content types.String, return content.IsNull() || (content.IsUnknown()) } -func normalizeRecordContentForState(recordType string, content string, dynamicIntent bool) types.String { +func normalizeRecordContentForState(recordType string, content string, dynamicIntent bool, host string) types.String { if dynamicIntent { return types.StringNull() } @@ -569,6 +600,9 @@ func normalizeRecordContentForState(recordType string, content string, dynamicIn return types.StringValue(addr.String()) } case "CNAME": + if trimmedHost := strings.TrimSpace(host); trimmedHost != "" { + return types.StringValue(strings.TrimSuffix(trimmedHost, ".")) + } return types.StringValue(strings.TrimSuffix(trimmed, ".")) } diff --git a/internal/provider/resource_dns_record_test.go b/internal/provider/resource_dns_record_test.go index 5b83406..4184882 100644 --- a/internal/provider/resource_dns_record_test.go +++ b/internal/provider/resource_dns_record_test.go @@ -146,17 +146,33 @@ func TestStringPointerFromOptionalContentForValidation(t *testing.T) { } func TestNormalizeRecordContentForState(t *testing.T) { - if got := normalizeRecordContentForState("AAAA", "2001:0db8:0000:0000:0000:0000:0000:0123", false); got.ValueString() != "2001:db8::123" { + 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()) } - if got := normalizeRecordContentForState("CNAME", "Example.COM.", false); got.ValueString() != "Example.COM" { + if got := normalizeRecordContentForState("CNAME", "Example.COM.", false, ""); got.ValueString() != "Example.COM" { t.Fatalf("expected trailing dot removed, got %q", got.ValueString()) } - if got := normalizeRecordContentForState("A", "(167.179.167.166)", true); !got.IsNull() { + if got := normalizeRecordContentForState("CNAME", "old.example.com", false, "new.example.co."); got.ValueString() != "new.example.co" { + t.Fatalf("expected CNAME content to come from host, got %q", got.ValueString()) + } + if got := normalizeRecordContentForState("A", "(167.179.167.166)", true, ""); !got.IsNull() { t.Fatalf("expected dynamic content to remain null, got %q", got.ValueString()) } } +func TestNormalizeDNSRecordUpdateRequestForType(t *testing.T) { + oldHost := "example.com" + newContent := "example.co" + req := normalizeDNSRecordUpdateRequestForType(dynuclient.UpdateDNSRecordRequest{ + RecordType: "CNAME", + Content: &newContent, + Host: oldHost, + }) + if req.Host != "example.co" { + t.Fatalf("expected CNAME host to be normalized from content, got %q", req.Host) + } +} + func TestInferDynamicIntentFromState(t *testing.T) { tests := []struct { name string