diff --git a/internal/stackitprovider/helper.go b/internal/stackitprovider/helper.go index d54cf09..a551fc5 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 == txtRecord { + 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 == txtRecord { + content = formatTXTContent(content) + } + records[i] = stackitdnsclient.RecordPayload{ - Content: change.Targets[i], + Content: content, } } @@ -126,3 +138,32 @@ 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 content + } + + 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.Contains(content, `" "`) { + return strings.ReplaceAll(content, `" "`, ``) + } + + return content +} diff --git a/internal/stackitprovider/helper_test.go b/internal/stackitprovider/helper_test.go index 14fa489..f23672b 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,100 @@ 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 unquoted", + content: string255, + want: string255, + }, + { + name: "Exactly 255 characters quoted", + 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) + } + }) + } +} diff --git a/internal/stackitprovider/records.go b/internal/stackitprovider/records.go index 5b66d9b..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) @@ -114,7 +116,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 == txtRecord { + content = unformatTXTContent(content) + } + + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, recordType, ttl, content)) } return endpoints