From da35eea597e8d9c5a5e405c03acf5b677c1fdc42 Mon Sep 17 00:00:00 2001 From: Florian Sandel Date: Tue, 30 Dec 2025 14:33:12 +0100 Subject: [PATCH 1/3] first implementation cleanup --- .../internal/services/dns/zone/resource.go | 36 ++++++++++++-- .../services/dns/zone/resource_test.go | 47 +++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/dns/zone/resource.go b/stackit/internal/services/dns/zone/resource.go index b7c76e11f..faad08cbe 100644 --- a/stackit/internal/services/dns/zone/resource.go +++ b/stackit/internal/services/dns/zone/resource.go @@ -537,10 +537,9 @@ func mapFields(ctx context.Context, zoneResp *dns.ZoneResponse, model *Model) er model.Active = types.BoolPointerValue(z.Active) model.ContactEmail = types.StringPointerValue(z.ContactEmail) model.DefaultTTL = types.Int64PointerValue(z.DefaultTTL) - model.DnsName = types.StringPointerValue(z.DnsName) model.ExpireTime = types.Int64PointerValue(z.ExpireTime) model.IsReverseZone = types.BoolPointerValue(z.IsReverseZone) - model.Name = types.StringPointerValue(z.Name) + model.DnsName = reconcileDnsName(model.DnsName, z.DnsName) model.NegativeCache = types.Int64PointerValue(z.NegativeCache) model.PrimaryNameServer = types.StringPointerValue(z.PrimaryNameServer) model.RecordCount = types.Int64PointerValue(rc) @@ -566,9 +565,16 @@ func toCreatePayload(model *Model) (*dns.CreateZonePayload, error) { } modelPrimaries = append(modelPrimaries, primaryString.ValueString()) } + + dnsName := conversion.StringValueToPointer(model.DnsName) + if dnsName != nil && strings.HasSuffix(*dnsName, ".") { + trimmed := strings.TrimSuffix(*dnsName, ".") + dnsName = &trimmed + } + return &dns.CreateZonePayload{ Name: conversion.StringValueToPointer(model.Name), - DnsName: conversion.StringValueToPointer(model.DnsName), + DnsName: dnsName, ContactEmail: conversion.StringValueToPointer(model.ContactEmail), Description: conversion.StringValueToPointer(model.Description), Acl: conversion.StringValueToPointer(model.Acl), @@ -601,3 +607,27 @@ func toUpdatePayload(model *Model) (*dns.PartialUpdateZonePayload, error) { Primaries: nil, // API returns error if this field is set, even if nothing changes }, nil } + +func reconcileDnsName(stateDnsName types.String, apiDnsName *string) types.String { + if apiDnsName == nil { + return types.StringNull() + } + + apiValue := *apiDnsName + + if stateDnsName.IsNull() || stateDnsName.IsUnknown() { + return types.StringValue(apiValue) + } + + stateValue := stateDnsName.ValueString() + + // If state has trailing dot, but API doesn't + if strings.HasSuffix(stateValue, ".") && !strings.HasSuffix(apiValue, ".") { + trimmedState := strings.TrimSuffix(stateValue, ".") + if trimmedState == apiValue { + return stateDnsName // Keep the state value with the dot + } + } + + return types.StringValue(apiValue) +} diff --git a/stackit/internal/services/dns/zone/resource_test.go b/stackit/internal/services/dns/zone/resource_test.go index d12cd90de..5d059f1ba 100644 --- a/stackit/internal/services/dns/zone/resource_test.go +++ b/stackit/internal/services/dns/zone/resource_test.go @@ -185,6 +185,40 @@ func TestMapFields(t *testing.T) { }, true, }, + { + "preserve_trailing_dot", + Model{ + ProjectId: types.StringValue("pid"), + ZoneId: types.StringValue("zid"), + DnsName: types.StringValue("example.com."), + }, + &dns.ZoneResponse{ + Zone: &dns.Zone{ + Id: utils.Ptr("zid"), + DnsName: utils.Ptr("example.com"), + }, + }, + Model{ + Id: types.StringValue("pid,zid"), + ProjectId: types.StringValue("pid"), + ZoneId: types.StringValue("zid"), + Name: types.StringNull(), + DnsName: types.StringValue("example.com."), + Acl: types.StringNull(), + DefaultTTL: types.Int64Null(), + ExpireTime: types.Int64Null(), + RefreshTime: types.Int64Null(), + RetryTime: types.Int64Null(), + SerialNumber: types.Int64Null(), + NegativeCache: types.Int64Null(), + Type: types.StringValue(""), + State: types.StringValue(""), + PrimaryNameServer: types.StringNull(), + Primaries: types.ListNull(types.StringType), + Visibility: types.StringValue(""), + }, + true, + }, { "nullable_fields_and_int_conversions_ok", Model{ @@ -345,6 +379,19 @@ func TestToCreatePayload(t *testing.T) { nil, false, }, + { + "with_trailing_dot", + &Model{ + Name: types.StringValue("Name"), + DnsName: types.StringValue("example.com."), + }, + &dns.CreateZonePayload{ + Name: utils.Ptr("Name"), + DnsName: utils.Ptr("example.com"), + Primaries: &[]string{}, + }, + true, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { From 837705262a25569c929adb57dd43762b8c3c69ec Mon Sep 17 00:00:00 2001 From: Florian Sandel Date: Fri, 2 Jan 2026 14:54:27 +0100 Subject: [PATCH 2/3] add acceptance test --- stackit/internal/services/dns/dns_acc_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/stackit/internal/services/dns/dns_acc_test.go b/stackit/internal/services/dns/dns_acc_test.go index 201ab118a..607737f55 100644 --- a/stackit/internal/services/dns/dns_acc_test.go +++ b/stackit/internal/services/dns/dns_acc_test.go @@ -83,6 +83,8 @@ func configVarsMaxUpdated() config.Variables { } func TestAccDnsMinResource(t *testing.T) { + dotDnsName := "tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha) + ".example.home." + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckDnsDestroy, @@ -247,6 +249,20 @@ func TestAccDnsMinResource(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "fqdn"), resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "state")), }, + // Test trailing dot preservation + { + Config: resourceMinConfig, + ConfigVariables: func() config.Variables { + vars := maps.Clone(testConfigVarsMin) + vars["dns_name"] = config.StringVariable(dotDnsName) + return vars + }(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_dns_zone.zone", "project_id", testutil.ProjectId), + // Verify that the state has the trailing dot, matching the config + resource.TestCheckResourceAttr("stackit_dns_zone.zone", "dns_name", dotDnsName), + ), + }, // Deletion is done by the framework implicitly }, }) From 198d9faac0a17f7461479059e71416f783b706a8 Mon Sep 17 00:00:00 2001 From: Florian Sandel Date: Fri, 2 Jan 2026 15:27:43 +0100 Subject: [PATCH 3/3] restore name mapping --- stackit/internal/services/dns/zone/resource.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stackit/internal/services/dns/zone/resource.go b/stackit/internal/services/dns/zone/resource.go index faad08cbe..c43f86cf3 100644 --- a/stackit/internal/services/dns/zone/resource.go +++ b/stackit/internal/services/dns/zone/resource.go @@ -537,9 +537,10 @@ func mapFields(ctx context.Context, zoneResp *dns.ZoneResponse, model *Model) er model.Active = types.BoolPointerValue(z.Active) model.ContactEmail = types.StringPointerValue(z.ContactEmail) model.DefaultTTL = types.Int64PointerValue(z.DefaultTTL) + model.DnsName = reconcileDnsName(model.DnsName, z.DnsName) model.ExpireTime = types.Int64PointerValue(z.ExpireTime) model.IsReverseZone = types.BoolPointerValue(z.IsReverseZone) - model.DnsName = reconcileDnsName(model.DnsName, z.DnsName) + model.Name = types.StringPointerValue(z.Name) model.NegativeCache = types.Int64PointerValue(z.NegativeCache) model.PrimaryNameServer = types.StringPointerValue(z.PrimaryNameServer) model.RecordCount = types.Int64PointerValue(rc)