From a418182868d574752d589e883b72099141d14f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20B=C4=9Bh=C3=A1vka?= Date: Thu, 7 May 2026 14:02:48 +0200 Subject: [PATCH 1/4] fix: split long TXT records into 255-character chunks --- internal/stackitprovider/helper.go | 46 +++++++++++++++++++++++++++-- internal/stackitprovider/records.go | 7 ++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/internal/stackitprovider/helper.go b/internal/stackitprovider/helper.go index d54cf09..a905f48 100644 --- a/internal/stackitprovider/helper.go +++ b/internal/stackitprovider/helper.go @@ -71,8 +71,14 @@ func modifyChange(change *endpoint.Endpoint) { func getStackitRecordSetPayload(change *endpoint.Endpoint) stackitdnsclient.CreateRecordSetPayload { records := make([]stackitdnsclient.RecordPayload, len(change.Targets)) for i := range change.Targets { + content := change.Targets[i] + + if change.RecordType == "TXT" { + content = formatTXTContent(content) + } + records[i] = stackitdnsclient.RecordPayload{ - Content: change.Targets[i], + Content: content, } } @@ -88,8 +94,14 @@ func getStackitRecordSetPayload(change *endpoint.Endpoint) stackitdnsclient.Crea func getStackitPartialUpdateRecordSetPayload(change *endpoint.Endpoint) stackitdnsclient.PartialUpdateRecordSetPayload { records := make([]stackitdnsclient.RecordPayload, len(change.Targets)) for i := range change.Targets { + content := change.Targets[i] + + if change.RecordType == "TXT" { + content = formatTXTContent(content) + } + records[i] = stackitdnsclient.RecordPayload{ - Content: change.Targets[i], + Content: content, } } @@ -126,3 +138,33 @@ func safeTTLToInt32(ttl endpoint.TTL) *int32 { return &v } + +// formatTXTContent splits long TXT records into 255-character chunks separated by spaces +func formatTXTContent(content string) string { + cleanContent := strings.Trim(content, "\"") + + if len(cleanContent) <= 255 { + return `"` + cleanContent + `"` + } + + var chunks []string + for i := 0; i < len(cleanContent); i += 255 { + end := i + 255 + if end > len(cleanContent) { + end = len(cleanContent) + } + chunks = append(chunks, `"`+cleanContent[i:end]+`"`) + } + + return strings.Join(chunks, " ") +} + +// unformatTXTContent reverses the DNS chunking and quoting process +func unformatTXTContent(content string) string { + if !strings.HasPrefix(content, "\"") || !strings.HasSuffix(content, "\"") { + return content + } + + trimmed := content[1 : len(content)-1] + return strings.ReplaceAll(trimmed, `" "`, "") +} diff --git a/internal/stackitprovider/records.go b/internal/stackitprovider/records.go index 5b66d9b..8b4cb3c 100644 --- a/internal/stackitprovider/records.go +++ b/internal/stackitprovider/records.go @@ -114,7 +114,12 @@ func endpointsFromRecords(name, recordType string, ttl endpoint.TTL, records []s for i := range records { rec := &records[i] - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, recordType, ttl, rec.Content)) + content := rec.Content + if recordType == "TXT" { + content = unformatTXTContent(content) + } + + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, recordType, ttl, content)) } return endpoints From b93c30e35660877e89a95aece37894f6adbd7748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20B=C4=9Bh=C3=A1vka?= Date: Thu, 7 May 2026 14:14:35 +0200 Subject: [PATCH 2/4] lint: use constant for TXT record type --- internal/stackitprovider/helper.go | 9 +++++---- internal/stackitprovider/records.go | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/stackitprovider/helper.go b/internal/stackitprovider/helper.go index a905f48..1ef6a33 100644 --- a/internal/stackitprovider/helper.go +++ b/internal/stackitprovider/helper.go @@ -73,7 +73,7 @@ func getStackitRecordSetPayload(change *endpoint.Endpoint) stackitdnsclient.Crea for i := range change.Targets { content := change.Targets[i] - if change.RecordType == "TXT" { + if change.RecordType == txtRecord { content = formatTXTContent(content) } @@ -96,7 +96,7 @@ func getStackitPartialUpdateRecordSetPayload(change *endpoint.Endpoint) stackitd for i := range change.Targets { content := change.Targets[i] - if change.RecordType == "TXT" { + if change.RecordType == txtRecord { content = formatTXTContent(content) } @@ -139,7 +139,7 @@ func safeTTLToInt32(ttl endpoint.TTL) *int32 { return &v } -// formatTXTContent splits long TXT records into 255-character chunks separated by spaces +// formatTXTContent splits long TXT records into 255-character chunks separated by spaces. func formatTXTContent(content string) string { cleanContent := strings.Trim(content, "\"") @@ -159,12 +159,13 @@ func formatTXTContent(content string) string { return strings.Join(chunks, " ") } -// unformatTXTContent reverses the DNS chunking and quoting process +// unformatTXTContent reverses the DNS chunking and quoting process. func unformatTXTContent(content string) string { if !strings.HasPrefix(content, "\"") || !strings.HasSuffix(content, "\"") { return content } trimmed := content[1 : len(content)-1] + return strings.ReplaceAll(trimmed, `" "`, "") } diff --git a/internal/stackitprovider/records.go b/internal/stackitprovider/records.go index 8b4cb3c..4be17a3 100644 --- a/internal/stackitprovider/records.go +++ b/internal/stackitprovider/records.go @@ -8,6 +8,8 @@ import ( "sigs.k8s.io/external-dns/provider" ) +const txtRecord = "TXT" + // Records returns resource records. func (d *StackitDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { zones, err := d.zoneFetcherClient.zones(ctx) @@ -115,7 +117,7 @@ func endpointsFromRecords(name, recordType string, ttl endpoint.TTL, records []s rec := &records[i] content := rec.Content - if recordType == "TXT" { + if recordType == txtRecord { content = unformatTXTContent(content) } From d0f25e5e9c786173356c2e6eb5e84da25d75fff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20B=C4=9Bh=C3=A1vka?= Date: Thu, 7 May 2026 16:15:20 +0200 Subject: [PATCH 3/4] feat: add unit tests for the helper function --- internal/stackitprovider/helper_test.go | 93 +++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/internal/stackitprovider/helper_test.go b/internal/stackitprovider/helper_test.go index 14fa489..4834e80 100644 --- a/internal/stackitprovider/helper_test.go +++ b/internal/stackitprovider/helper_test.go @@ -2,6 +2,7 @@ package stackitprovider import ( "reflect" + "strings" "testing" stackitdnsclient "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" @@ -214,3 +215,95 @@ func TestGetStackitRRSetRecordPatch(t *testing.T) { t.Errorf("getStackitRRSetRecordPatch() = %v, want %v", got, expected) } } + +func TestFormatTXTContent(t *testing.T) { + t.Parallel() + + // Generate strings of exact lengths for testing + string255 := strings.Repeat("a", 255) + string256 := strings.Repeat("a", 256) + string511 := strings.Repeat("a", 511) + + tests := []struct { + name string + content string + want string + }{ + { + name: "Short string without quotes", + content: "hello world", + want: `"hello world"`, + }, + { + name: "Short string with existing quotes", + content: `"hello world"`, + want: `"hello world"`, + }, + { + name: "Exactly 255 characters", + content: string255, + want: `"` + string255 + `"`, + }, + { + name: "256 characters (requires 2 chunks)", + content: string256, + want: `"` + string255 + `" "a"`, + }, + { + name: "511 characters (requires 3 chunks)", + content: string511, + want: `"` + string255 + `" "` + string255 + `" "a"`, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := formatTXTContent(tt.content); got != tt.want { + t.Errorf("formatTXTContent() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUnformatTXTContent(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content string + want string + }{ + { + name: "Unquoted short string", + content: "hello world", + want: "hello world", + }, + { + name: "Single chunk quoted string", + content: `"hello world"`, + want: "hello world", + }, + { + name: "Two chunk string", + content: `"hello" "world"`, + want: "helloworld", + }, + { + name: "Three chunk string", + content: `"chunk1" "chunk2" "chunk3"`, + want: "chunk1chunk2chunk3", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := unformatTXTContent(tt.content); got != tt.want { + t.Errorf("unformatTXTContent() = %v, want %v", got, tt.want) + } + }) + } +} From 980846ab2bdbe7e5495b2a84272593f55998f1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20B=C4=9Bh=C3=A1vka?= Date: Fri, 8 May 2026 11:13:33 +0200 Subject: [PATCH 4/4] fix: adjust TXT record formatting to preserve quotes for improved consistency --- internal/stackitprovider/helper.go | 12 +++++------- internal/stackitprovider/helper_test.go | 15 ++++++++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/internal/stackitprovider/helper.go b/internal/stackitprovider/helper.go index 1ef6a33..a551fc5 100644 --- a/internal/stackitprovider/helper.go +++ b/internal/stackitprovider/helper.go @@ -141,10 +141,10 @@ func safeTTLToInt32(ttl endpoint.TTL) *int32 { // formatTXTContent splits long TXT records into 255-character chunks separated by spaces. func formatTXTContent(content string) string { - cleanContent := strings.Trim(content, "\"") + cleanContent := strings.Trim(content, `"`) if len(cleanContent) <= 255 { - return `"` + cleanContent + `"` + return content } var chunks []string @@ -161,11 +161,9 @@ func formatTXTContent(content string) string { // unformatTXTContent reverses the DNS chunking and quoting process. func unformatTXTContent(content string) string { - if !strings.HasPrefix(content, "\"") || !strings.HasSuffix(content, "\"") { - return content + if strings.Contains(content, `" "`) { + return strings.ReplaceAll(content, `" "`, ``) } - trimmed := content[1 : len(content)-1] - - return strings.ReplaceAll(trimmed, `" "`, "") + return content } diff --git a/internal/stackitprovider/helper_test.go b/internal/stackitprovider/helper_test.go index 4834e80..f23672b 100644 --- a/internal/stackitprovider/helper_test.go +++ b/internal/stackitprovider/helper_test.go @@ -232,7 +232,7 @@ func TestFormatTXTContent(t *testing.T) { { name: "Short string without quotes", content: "hello world", - want: `"hello world"`, + want: "hello world", }, { name: "Short string with existing quotes", @@ -240,8 +240,13 @@ func TestFormatTXTContent(t *testing.T) { want: `"hello world"`, }, { - name: "Exactly 255 characters", + name: "Exactly 255 characters unquoted", content: string255, + want: string255, + }, + { + name: "Exactly 255 characters quoted", + content: `"` + string255 + `"`, want: `"` + string255 + `"`, }, { @@ -283,17 +288,17 @@ func TestUnformatTXTContent(t *testing.T) { { name: "Single chunk quoted string", content: `"hello world"`, - want: "hello world", + want: `"hello world"`, }, { name: "Two chunk string", content: `"hello" "world"`, - want: "helloworld", + want: `"helloworld"`, }, { name: "Three chunk string", content: `"chunk1" "chunk2" "chunk3"`, - want: "chunk1chunk2chunk3", + want: `"chunk1chunk2chunk3"`, }, }