From ef9998311899073d22038e60bda9afa761ecbf68 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Wed, 29 Apr 2026 15:43:29 +1000 Subject: [PATCH 1/2] Rename dns record state field to enabled --- README.md | 14 ++-- examples/live_safe_dns_record/README.md | 13 ++-- examples/live_safe_dns_record/main.tf | 14 ++-- .../terraform.tfvars.example | 1 + examples/live_safe_dns_record/variables.tf | 8 +- .../resources/dynu_dns_record/resource.tf | 11 +-- internal/dynuclient/client.go | 13 +++- internal/provider/resource_dns_record.go | 77 +++++++++++++++++-- .../resource_dns_record_integration_test.go | 12 +-- internal/provider/resource_dns_record_test.go | 30 ++++++++ 10 files changed, 156 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 29c59b4..9a307bb 100644 --- a/README.md +++ b/README.md @@ -163,9 +163,10 @@ Arguments: - `dynamic` (Bool, optional/computed) - Explicit dynamic-mode toggle for `A`/`AAAA`. Existing omitted `content` behavior remains backward compatible. - `ttl` (Number, optional) -- `state` (Bool, optional) +- `enabled` (Bool, optional, defaults to `true`) - `group` (String, optional) - `host` (String, optional) +- `location` (String, optional; A/AAAA only) - `node_name` (String, optional) Attributes: @@ -180,10 +181,11 @@ Example: ```hcl resource "dynu_dns_record" "txt" { hostname = "api.example.com" - record_type = "TXT" - content = "hello-from-terraform" - ttl = 300 - state = true + record_type = "A" + content = "198.51.100.10" + ttl = 90 + enabled = false + location = "us" } resource "dynu_dns_record" "dynamic_a" { @@ -244,7 +246,7 @@ Attributes: - `domain_name` (String) - `records` (List(Object)) with: - `id`, `domain_id`, `domain_name`, `node_name`, `hostname`, `record_type` - - `ttl`, `state`, `content`, `updated_on`, `group`, `host` + - `ttl`, `state`, `content`, `updated_on`, `group`, `host`, `location` Example: diff --git a/examples/live_safe_dns_record/README.md b/examples/live_safe_dns_record/README.md index 8db8848..d89d3ee 100644 --- a/examples/live_safe_dns_record/README.md +++ b/examples/live_safe_dns_record/README.md @@ -6,9 +6,9 @@ This example is an **opt-in live test** for writable Dynu provider functionality Using a single suffix (`test_suffix`), this example creates five DNS record scenarios under `dynu_root_domain`: -1. `A` record with IPv4 content (`codex-a-.`) -2. `AAAA` record with IPv6 content (`codex-aaaa-.`) -3. `CNAME` record (`codex-cname-.`) +1. `A` record with IPv4 content, `location`, and minimum TTL=90 (`codex-a-.`) +2. `AAAA` record with IPv6 content and `location` (`codex-aaaa-.`) +3. `CNAME` record created disabled (`enabled = false`) (`codex-cname-.`) 4. **Dynamic `A` record** with omitted content (`codex-dynamic-a-.`) 5. **Dynamic `AAAA` record** with omitted content (`codex-dynamic-aaaa-.`) @@ -32,7 +32,10 @@ Edit `terraform.tfvars` and set at least: - `dynu_root_domain` - `test_suffix` (use a unique value per run) -Optional overrides include `test_ipv4`, `test_ipv6`, and `test_cname_target`. Use real routable IPs for `test_ipv4`/`test_ipv6`; Dynu rejects documentation ranges such as `192.0.2.0/24` and `2001:db8::/32`. +Optional overrides include `test_ipv4`, `test_ipv6`, `test_cname_target`, `test_ttl`, and `test_location`. + +- `test_ttl` must be `0` (provider/API default) or `>= 90`. +- `test_location` applies only to A/AAAA records. ## Run @@ -46,7 +49,7 @@ terraform destroy ## Notes -- `terraform apply` should create all five record scenarios. +- `terraform apply` should create all five record scenarios (including a disabled CNAME example). - `terraform destroy` should remove all five records created by this state. - If you need to target a single scenario, resources are explicitly named: - `dynu_dns_record.a_ipv4` diff --git a/examples/live_safe_dns_record/main.tf b/examples/live_safe_dns_record/main.tf index 33df823..2487ce2 100644 --- a/examples/live_safe_dns_record/main.tf +++ b/examples/live_safe_dns_record/main.tf @@ -17,8 +17,9 @@ resource "dynu_dns_record" "a_ipv4" { hostname = local.hostname_a_ipv4 record_type = "A" content = var.test_ipv4 - ttl = var.test_ttl - state = true + ttl = 90 + enabled = true + location = var.test_location } resource "dynu_dns_record" "aaaa_ipv6" { @@ -26,7 +27,8 @@ resource "dynu_dns_record" "aaaa_ipv6" { record_type = "AAAA" content = var.test_ipv6 ttl = var.test_ttl - state = true + enabled = true + location = var.test_location } resource "dynu_dns_record" "cname" { @@ -34,7 +36,7 @@ resource "dynu_dns_record" "cname" { record_type = "CNAME" content = var.test_cname_target ttl = var.test_ttl - state = true + enabled = false } # Deliberate dynamic A record scenario: content intentionally omitted for Dynu dynamic IPv4 behavior. @@ -42,7 +44,7 @@ resource "dynu_dns_record" "dynamic_a" { hostname = local.hostname_dynamic_a record_type = "A" ttl = var.test_ttl - state = true + enabled = true } # Deliberate dynamic AAAA record scenario: content intentionally omitted for Dynu dynamic IPv6 behavior. @@ -50,5 +52,5 @@ resource "dynu_dns_record" "dynamic_aaaa" { hostname = local.hostname_dynamic_aaaa record_type = "AAAA" ttl = var.test_ttl - state = true + enabled = true } diff --git a/examples/live_safe_dns_record/terraform.tfvars.example b/examples/live_safe_dns_record/terraform.tfvars.example index 2c75fac..6dc6ee0 100644 --- a/examples/live_safe_dns_record/terraform.tfvars.example +++ b/examples/live_safe_dns_record/terraform.tfvars.example @@ -14,3 +14,4 @@ test_ipv6 = "2001:db8::123" test_cname_target = "example.com" test_ttl = 300 +test_location = "us" diff --git a/examples/live_safe_dns_record/variables.tf b/examples/live_safe_dns_record/variables.tf index b0e1a9c..19fff2e 100644 --- a/examples/live_safe_dns_record/variables.tf +++ b/examples/live_safe_dns_record/variables.tf @@ -60,7 +60,13 @@ variable "test_cname_target" { } variable "test_ttl" { - description = "TTL in seconds for disposable DNS records." + description = "TTL in seconds for disposable DNS records. Must be 0 (provider/API default) or >= 90." type = number default = 300 } + +variable "test_location" { + description = "Optional Dynu location hint for A/AAAA records." + type = string + default = "us" +} diff --git a/examples/resources/dynu_dns_record/resource.tf b/examples/resources/dynu_dns_record/resource.tf index 07cbce9..94b4809 100644 --- a/examples/resources/dynu_dns_record/resource.tf +++ b/examples/resources/dynu_dns_record/resource.tf @@ -1,7 +1,8 @@ -resource "dynu_dns_record" "txt" { +resource "dynu_dns_record" "a_location_min_ttl" { hostname = "api.example.com" - record_type = "TXT" - content = "managed-by-terraform" - ttl = 300 - state = true + record_type = "A" + content = "198.51.100.10" + ttl = 90 + enabled = false + location = "us" } diff --git a/internal/dynuclient/client.go b/internal/dynuclient/client.go index 5a67c61..28bbc0a 100644 --- a/internal/dynuclient/client.go +++ b/internal/dynuclient/client.go @@ -106,6 +106,7 @@ type DNSRecord struct { UpdatedOn string `json:"updatedOn"` Group string `json:"group"` Host string `json:"host"` + Location string `json:"location"` } type CreateDNSRecordRequest struct { @@ -116,6 +117,7 @@ type CreateDNSRecordRequest struct { State *bool `json:"state,omitempty"` Group string `json:"group,omitempty"` Host string `json:"host,omitempty"` + Location string `json:"location,omitempty"` } type UpdateDNSRecordRequest struct { @@ -126,6 +128,7 @@ type UpdateDNSRecordRequest struct { State *bool `json:"state,omitempty"` Group string `json:"group,omitempty"` Host string `json:"host,omitempty"` + Location string `json:"location,omitempty"` } type listDomainsResponse struct { @@ -207,7 +210,7 @@ func (c *Client) GetDNSRecord(ctx context.Context, domainID int64, recordID int6 func (c *Client) CreateDNSRecord(ctx context.Context, domainID int64, req CreateDNSRecordRequest) (*DNSRecord, error) { var resp getDNSRecordResponse - if err := c.doRequest(ctx, http.MethodPost, fmt.Sprintf("/dns/%d/record", domainID), buildDNSRecordUpsertPayload(req.RecordType, req.NodeName, req.Content, req.TTL, req.State, req.Group, req.Host), &resp); err != nil { + if err := c.doRequest(ctx, http.MethodPost, fmt.Sprintf("/dns/%d/record", domainID), buildDNSRecordUpsertPayload(req.RecordType, req.NodeName, req.Content, req.TTL, req.State, req.Group, req.Host, req.Location), &resp); err != nil { return nil, err } normalizeDNSRecord(&resp.DNSRecord) @@ -216,7 +219,7 @@ func (c *Client) CreateDNSRecord(ctx context.Context, domainID int64, req Create func (c *Client) UpdateDNSRecord(ctx context.Context, domainID int64, recordID int64, req UpdateDNSRecordRequest) (*DNSRecord, error) { var resp getDNSRecordResponse - if err := c.doRequest(ctx, http.MethodPost, fmt.Sprintf("/dns/%d/record/%d", domainID, recordID), buildDNSRecordUpsertPayload(req.RecordType, req.NodeName, req.Content, req.TTL, req.State, req.Group, req.Host), &resp); err != nil { + if err := c.doRequest(ctx, http.MethodPost, fmt.Sprintf("/dns/%d/record/%d", domainID, recordID), buildDNSRecordUpsertPayload(req.RecordType, req.NodeName, req.Content, req.TTL, req.State, req.Group, req.Host, req.Location), &resp); err != nil { return nil, err } normalizeDNSRecord(&resp.DNSRecord) @@ -319,9 +322,10 @@ type dnsRecordUpsertPayload struct { State *bool `json:"state,omitempty"` Group string `json:"group,omitempty"` Host string `json:"host,omitempty"` + Location string `json:"location,omitempty"` } -func buildDNSRecordUpsertPayload(recordType string, nodeName string, content *string, ttl int64, state *bool, group string, host string) dnsRecordUpsertPayload { +func buildDNSRecordUpsertPayload(recordType string, nodeName string, content *string, ttl int64, state *bool, group string, host string, location string) dnsRecordUpsertPayload { normalizedType := strings.ToUpper(strings.TrimSpace(recordType)) normalizedContent := normalizeOptionalContent(content) payload := dnsRecordUpsertPayload{ @@ -331,6 +335,7 @@ func buildDNSRecordUpsertPayload(recordType string, nodeName string, content *st State: state, Group: group, Host: host, + Location: location, } switch normalizedType { @@ -346,8 +351,10 @@ func buildDNSRecordUpsertPayload(recordType string, nodeName string, content *st if normalizedContent != nil { payload.Host = *normalizedContent } + payload.Location = "" default: payload.Content = normalizedContent + payload.Location = "" } return payload diff --git a/internal/provider/resource_dns_record.go b/internal/provider/resource_dns_record.go index db0c547..5242a67 100644 --- a/internal/provider/resource_dns_record.go +++ b/internal/provider/resource_dns_record.go @@ -4,15 +4,18 @@ import ( "context" "errors" "fmt" + "net" "net/netip" "strconv" "strings" + "time" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -37,9 +40,10 @@ type dnsRecordResourceModel struct { Content types.String `tfsdk:"content"` Dynamic types.Bool `tfsdk:"dynamic"` TTL types.Int64 `tfsdk:"ttl"` - State types.Bool `tfsdk:"state"` + Enabled types.Bool `tfsdk:"enabled"` Group types.String `tfsdk:"group"` Host types.String `tfsdk:"host"` + Location types.String `tfsdk:"location"` NodeName types.String `tfsdk:"node_name"` DomainID types.Int64 `tfsdk:"domain_id"` DomainName types.String `tfsdk:"domain_name"` @@ -76,9 +80,10 @@ func (r *dnsRecordResource) Schema(_ context.Context, _ resource.SchemaRequest, Description: "DNS TTL in seconds.", Validators: []validator.Int64{int64validator.AtLeast(0)}, }, - "state": schema.BoolAttribute{Optional: true, Computed: true, Description: "Whether this DNS record is active."}, + "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), Description: "Whether this DNS record is enabled/active."}, "group": schema.StringAttribute{Optional: true, Computed: true, Description: "Dynu group value for this record."}, "host": schema.StringAttribute{Optional: true, Computed: true, Description: "Host field for supported Dynu record types."}, + "location": schema.StringAttribute{Optional: true, Computed: true, Description: "Dynu location hint for A/AAAA records only."}, "node_name": schema.StringAttribute{Optional: true, Computed: true, Description: "Node/label portion of the record."}, "domain_id": schema.Int64Attribute{Computed: true, Description: "Dynu domain ID resolved from hostname."}, "domain_name": schema.StringAttribute{Computed: true, Description: "Dynu root domain name resolved from hostname."}, @@ -116,6 +121,8 @@ func (r *dnsRecordResource) ValidateConfig(ctx context.Context, req resource.Val return } validateDNSRecordContentForTypeWithKnowledge(recordType, content, contentKnown, dynamicIntent, &resp.Diagnostics) + validateDNSRecordTTL(config.TTL, &resp.Diagnostics) + validateDNSRecordLocation(recordType, config.Location, &resp.Diagnostics) } func (r *dnsRecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { @@ -142,14 +149,21 @@ func (r *dnsRecordResource) Create(ctx context.Context, req resource.CreateReque RecordType: recordType, Content: stringPointerFromOptionalContent(plan.Content), TTL: int64FromOptional(plan.TTL), - State: boolPointerFromOptional(plan.State), + State: boolPointerFromOptional(plan.Enabled), Group: stringFromOptional(plan.Group), Host: stringFromOptional(plan.Host), + Location: stringFromOptional(plan.Location), } createReq = normalizeDNSRecordCreateRequestForType(createReq) if !validateDNSRecordContentForType(createReq.RecordType, createReq.Content, dynamicIntent, &resp.Diagnostics) { return } + if !validateRecordTTLSeconds(createReq.TTL, &resp.Diagnostics) { + return + } + if !validateLocationForType(createReq.RecordType, createReq.Location, &resp.Diagnostics) { + return + } record, err := r.clientProvider.client.CreateDNSRecord(ctx, domainID, createReq) if err != nil && dynamicIntent && isUnsupportedEmptyContentError(err) { @@ -239,14 +253,21 @@ func (r *dnsRecordResource) Update(ctx context.Context, req resource.UpdateReque RecordType: recordType, Content: stringPointerFromOptionalContent(plan.Content), TTL: int64FromOptional(preferKnownInt64(plan.TTL, state.TTL)), - State: boolPointerFromOptional(preferKnownBool(plan.State, state.State)), + State: boolPointerFromOptional(preferKnownBool(plan.Enabled, state.Enabled)), Group: stringFromOptional(preferKnownString(plan.Group, state.Group)), Host: stringFromOptional(preferKnownString(plan.Host, state.Host)), + Location: stringFromOptional(preferKnownString(plan.Location, state.Location)), } updateReq = normalizeDNSRecordUpdateRequestForType(updateReq) if !validateDNSRecordContentForType(updateReq.RecordType, updateReq.Content, dynamicIntent, &resp.Diagnostics) { return } + if !validateRecordTTLSeconds(updateReq.TTL, &resp.Diagnostics) { + return + } + if !validateLocationForType(updateReq.RecordType, updateReq.Location, &resp.Diagnostics) { + return + } if _, err := r.clientProvider.client.UpdateDNSRecord(ctx, domainID, recordID, updateReq); err != nil { if dynamicIntent && isUnsupportedEmptyContentError(err) { @@ -258,6 +279,7 @@ func (r *dnsRecordResource) Update(ctx context.Context, req resource.UpdateReque State: updateReq.State, Group: updateReq.Group, Host: updateReq.Host, + Location: updateReq.Location, }, &resp.Diagnostics); retryOK { updateReq.Content = retryReq.Content updateReq.Group = retryReq.Group @@ -324,9 +346,10 @@ func mapDNSRecordToState(record dynuclient.DNSRecord, dynamicIntent bool) dnsRec Content: content, Dynamic: types.BoolValue(dynamicIntent), TTL: types.Int64Value(record.TTL), - State: types.BoolValue(record.State), + Enabled: types.BoolValue(record.State), Group: mapString(record.Group), Host: mapString(record.Host), + Location: mapString(record.Location), NodeName: mapString(record.NodeName), DomainID: types.Int64Value(record.DomainID), DomainName: mapString(record.DomainName), @@ -511,6 +534,16 @@ func validateDNSRecordContentForTypeWithKnowledge(recordType string, content *st } return true } + if normalizedType == "CNAME" { + if net.ParseIP(trimmedContent) != nil { + diagnostics.AddError("Invalid DNS record content", fmt.Sprintf("Record type %q requires a hostname target, got IP %q.", normalizedType, trimmedContent)) + return false + } + if strings.HasPrefix(strings.ToLower(trimmedContent), "http://") || strings.HasPrefix(strings.ToLower(trimmedContent), "https://") { + diagnostics.AddError("Invalid DNS record content", fmt.Sprintf("Record type %q target must not include a URL scheme: %q.", normalizedType, trimmedContent)) + return false + } + } if dynamicIntent { diagnostics.AddError( @@ -535,6 +568,40 @@ func validateDNSRecordContentForTypeWithKnowledge(recordType string, content *st return true } +func validateDNSRecordTTL(ttl types.Int64, diagnostics *diag.Diagnostics) { + if ttl.IsNull() || ttl.IsUnknown() { + return + } + validateRecordTTLSeconds(ttl.ValueInt64(), diagnostics) +} + +func validateRecordTTLSeconds(ttl int64, diagnostics *diag.Diagnostics) bool { + if ttl > 0 && ttl < int64((90*time.Second).Seconds()) { + diagnostics.AddError("Invalid TTL", fmt.Sprintf("invalid TTL %ds: Dynu requires TTL >= 90s", ttl)) + return false + } + return true +} + +func validateDNSRecordLocation(recordType string, location types.String, diagnostics *diag.Diagnostics) { + if location.IsNull() || location.IsUnknown() { + return + } + validateLocationForType(recordType, location.ValueString(), diagnostics) +} + +func validateLocationForType(recordType string, location string, diagnostics *diag.Diagnostics) bool { + if strings.TrimSpace(location) == "" { + return true + } + normalizedType := strings.ToUpper(strings.TrimSpace(recordType)) + if normalizedType != "A" && normalizedType != "AAAA" { + diagnostics.AddError("Invalid location", fmt.Sprintf("location is only supported for A and AAAA records, got %q.", normalizedType)) + return false + } + return true +} + func resolveDynamicIntent(recordType string, content types.String, dynamic types.Bool, diagnostics *diag.Diagnostics) (bool, bool) { normalizedType := strings.ToUpper(strings.TrimSpace(recordType)) contentPtr := stringPointerFromOptionalContent(content) diff --git a/internal/provider/resource_dns_record_integration_test.go b/internal/provider/resource_dns_record_integration_test.go index 05504c9..ab7e149 100644 --- a/internal/provider/resource_dns_record_integration_test.go +++ b/internal/provider/resource_dns_record_integration_test.go @@ -27,8 +27,8 @@ func TestIntegrationResourceDNSRecordLifecycleAndImport(t *testing.T) { Hostname: types.StringValue("api.a.example.com"), RecordType: types.StringValue("TXT"), Content: types.StringValue("created"), - TTL: types.Int64Value(60), - State: types.BoolValue(true), + TTL: types.Int64Value(90), + Enabled: types.BoolValue(true), Group: types.StringValue("test"), Host: types.StringNull(), NodeName: types.StringNull(), @@ -157,8 +157,8 @@ func TestIntegrationResourceDNSRecordDynamicAStateStableAndTransitionToStatic(t Hostname: types.StringValue("api.a.example.com"), RecordType: types.StringValue("A"), Content: types.StringNull(), - TTL: types.Int64Value(60), - State: types.BoolValue(true), + TTL: types.Int64Value(90), + Enabled: types.BoolValue(true), } plan := tfsdk.Plan{Schema: schemaResp.Schema} if diags := plan.Set(ctx, &createPlan); diags.HasError() { @@ -234,8 +234,8 @@ func TestIntegrationResourceDNSRecordUpdateUsesStateIDWhenPlanIDUnknown(t *testi Hostname: types.StringValue("api.a.example.com"), RecordType: types.StringValue("TXT"), Content: types.StringValue("v=one"), - TTL: types.Int64Value(60), - State: types.BoolValue(true), + TTL: types.Int64Value(90), + Enabled: types.BoolValue(true), Group: types.StringValue("test-group"), Host: types.StringValue("test-host"), NodeName: types.StringValue("test-node"), diff --git a/internal/provider/resource_dns_record_test.go b/internal/provider/resource_dns_record_test.go index 4184882..0df3dfa 100644 --- a/internal/provider/resource_dns_record_test.go +++ b/internal/provider/resource_dns_record_test.go @@ -173,6 +173,36 @@ func TestNormalizeDNSRecordUpdateRequestForType(t *testing.T) { } } +func TestValidateRecordTTLSeconds(t *testing.T) { + cases := []struct { + ttl int64 + want bool + }{ + {ttl: 0, want: true}, + {ttl: 89, want: false}, + {ttl: 90, want: true}, + {ttl: 1800, want: true}, + } + for _, tc := range cases { + diags := diag.Diagnostics{} + got := validateRecordTTLSeconds(tc.ttl, &diags) + if got != tc.want { + t.Fatalf("validateRecordTTLSeconds(%d)=%v, want %v", tc.ttl, got, tc.want) + } + } +} + +func TestValidateLocationForType(t *testing.T) { + diags := diag.Diagnostics{} + if !validateLocationForType("A", "us", &diags) || diags.HasError() { + t.Fatal("expected location for A to be valid") + } + diags = diag.Diagnostics{} + if validateLocationForType("CNAME", "us", &diags) || !diags.HasError() { + t.Fatal("expected location for CNAME to fail") + } +} + func TestInferDynamicIntentFromState(t *testing.T) { tests := []struct { name string From 37575ad09388e385e72ddc2acacc859adb31548e Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Wed, 29 Apr 2026 15:51:40 +1000 Subject: [PATCH 2/2] Fix location inheritance on record type transitions --- internal/provider/resource_dns_record.go | 10 +++++++++- internal/provider/resource_dns_record_test.go | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/internal/provider/resource_dns_record.go b/internal/provider/resource_dns_record.go index 5242a67..f872a23 100644 --- a/internal/provider/resource_dns_record.go +++ b/internal/provider/resource_dns_record.go @@ -256,7 +256,7 @@ func (r *dnsRecordResource) Update(ctx context.Context, req resource.UpdateReque State: boolPointerFromOptional(preferKnownBool(plan.Enabled, state.Enabled)), Group: stringFromOptional(preferKnownString(plan.Group, state.Group)), Host: stringFromOptional(preferKnownString(plan.Host, state.Host)), - Location: stringFromOptional(preferKnownString(plan.Location, state.Location)), + Location: locationForUpdate(recordType, plan.Location, state.Location), } updateReq = normalizeDNSRecordUpdateRequestForType(updateReq) if !validateDNSRecordContentForType(updateReq.RecordType, updateReq.Content, dynamicIntent, &resp.Diagnostics) { @@ -602,6 +602,14 @@ func validateLocationForType(recordType string, location string, diagnostics *di return true } +func locationForUpdate(recordType string, planLocation types.String, stateLocation types.String) string { + normalizedType := strings.ToUpper(strings.TrimSpace(recordType)) + if normalizedType != "A" && normalizedType != "AAAA" { + return stringFromOptional(planLocation) + } + return stringFromOptional(preferKnownString(planLocation, stateLocation)) +} + func resolveDynamicIntent(recordType string, content types.String, dynamic types.Bool, diagnostics *diag.Diagnostics) (bool, bool) { normalizedType := strings.ToUpper(strings.TrimSpace(recordType)) contentPtr := stringPointerFromOptionalContent(content) diff --git a/internal/provider/resource_dns_record_test.go b/internal/provider/resource_dns_record_test.go index 0df3dfa..c6c35f4 100644 --- a/internal/provider/resource_dns_record_test.go +++ b/internal/provider/resource_dns_record_test.go @@ -203,6 +203,18 @@ func TestValidateLocationForType(t *testing.T) { } } +func TestLocationForUpdate(t *testing.T) { + if got := locationForUpdate("A", types.StringNull(), types.StringValue("us")); got != "us" { + t.Fatalf("expected A record update to preserve state location, got %q", got) + } + if got := locationForUpdate("CNAME", types.StringNull(), types.StringValue("us")); got != "" { + t.Fatalf("expected non-A/AAAA type to ignore prior state location when omitted, got %q", got) + } + if got := locationForUpdate("TXT", types.StringValue("eu"), types.StringValue("us")); got != "eu" { + t.Fatalf("expected explicit plan location to be returned as-is, got %q", got) + } +} + func TestInferDynamicIntentFromState(t *testing.T) { tests := []struct { name string