Skip to content

Commit 28f7db3

Browse files
authored
feat: Support dns TXT records with more than 255 characters (#580)
* add support for updating DNS TXT records with values > 255 characters * fix: review feedback - create new function which formats the dns records - add test cases - moved the formatting of txt records - in create command to parseInput - in update to separate function
1 parent fd12918 commit 28f7db3

File tree

6 files changed

+314
-8
lines changed

6 files changed

+314
-8
lines changed

internal/cmd/dns/record-set/create/create.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77

88
"github.com/goccy/go-yaml"
9+
"github.com/spf13/cobra"
910
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
1011
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
1112
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
@@ -16,8 +17,6 @@ import (
1617
dnsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/dns/utils"
1718
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
1819
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
19-
20-
"github.com/spf13/cobra"
2120
"github.com/stackitcloud/stackit-sdk-go/services/dns"
2221
"github.com/stackitcloud/stackit-sdk-go/services/dns/wait"
2322
)
@@ -31,6 +30,7 @@ const (
3130
typeFlag = "type"
3231

3332
defaultType = "A"
33+
txtType = "TXT"
3434
)
3535

3636
type inputModel struct {
@@ -137,6 +137,20 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
137137
Type: flags.FlagWithDefaultToStringValue(p, cmd, typeFlag),
138138
}
139139

140+
if model.Type == txtType {
141+
for idx := range model.Records {
142+
// Based on RFC 1035 section 2.3.4, TXT Records are limited to 255 Characters
143+
// Longer strings need to be split into multiple records
144+
if len(model.Records[idx]) > 255 {
145+
var err error
146+
model.Records[idx], err = dnsUtils.FormatTxtRecord(model.Records[idx])
147+
if err != nil {
148+
return nil, err
149+
}
150+
}
151+
}
152+
}
153+
140154
if p.IsVerbosityDebug() {
141155
modelStr, err := print.BuildDebugStrFromInputModel(model)
142156
if err != nil {

internal/cmd/dns/record-set/create/create_test.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package create
22

33
import (
44
"context"
5+
"fmt"
6+
"strings"
57
"testing"
68

79
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
@@ -23,6 +25,12 @@ var testClient = &dns.APIClient{}
2325
var testProjectId = uuid.NewString()
2426
var testZoneId = uuid.NewString()
2527

28+
var recordTxtOver255Char = []string{
29+
"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo",
30+
"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo",
31+
"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar",
32+
}
33+
2634
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
2735
flagValues := map[string]string{
2836
projectIdFlag: testProjectId,
@@ -76,7 +84,7 @@ func fixtureRequest(mods ...func(request *dns.ApiCreateRecordSetRequest)) dns.Ap
7684
}
7785

7886
func TestParseInput(t *testing.T) {
79-
tests := []struct {
87+
var tests = []struct {
8088
description string
8189
flagValues map[string]string
8290
recordFlagValues []string
@@ -236,8 +244,27 @@ func TestParseInput(t *testing.T) {
236244
model.Records = append(model.Records, "1.2.3.4", "5.6.7.8")
237245
}),
238246
},
239-
}
247+
{
248+
description: "TXT record with > 255 characters",
249+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
250+
flagValues[typeFlag] = txtType
251+
flagValues[recordFlag] = strings.Join(recordTxtOver255Char, "")
252+
}),
253+
isValid: true,
254+
expectedModel: fixtureInputModel(func(model *inputModel) {
255+
var content string
256+
for idx, val := range recordTxtOver255Char {
257+
content += fmt.Sprintf("%q", val)
258+
if idx != len(recordTxtOver255Char)-1 {
259+
content += " "
260+
}
261+
}
240262

263+
model.Records = []string{content}
264+
model.Type = txtType
265+
}),
266+
},
267+
}
241268
for _, tt := range tests {
242269
t.Run(tt.description, func(t *testing.T) {
243270
p := print.NewPrinter()

internal/cmd/dns/record-set/update/update.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const (
2828
nameFlag = "name"
2929
recordFlag = "record"
3030
ttlFlag = "ttl"
31+
txtType = "TXT"
3132
)
3233

3334
type inputModel struct {
@@ -38,6 +39,7 @@ type inputModel struct {
3839
Name *string
3940
Records *[]string
4041
TTL *int64
42+
Type *string
4143
}
4244

4345
func NewCmd(p *print.Printer) *cobra.Command {
@@ -76,6 +78,19 @@ func NewCmd(p *print.Printer) *cobra.Command {
7678
recordSetLabel = model.RecordSetId
7779
}
7880

81+
typeLabel, err := dnsUtils.GetRecordSetType(ctx, apiClient, model.ProjectId, model.ZoneId, model.RecordSetId)
82+
if err != nil {
83+
p.Debug(print.ErrorLevel, "get record set type: %v", err)
84+
}
85+
model.Type = typeLabel
86+
87+
if utils.PtrString(model.Type) == txtType {
88+
err = parseTxtRecord(model.Records)
89+
if err != nil {
90+
return err
91+
}
92+
}
93+
7994
if !model.AssumeYes {
8095
prompt := fmt.Sprintf("Are you sure you want to update record set %s of zone %s?", recordSetLabel, zoneLabel)
8196
err = p.PromptForConfirmation(prompt)
@@ -165,6 +180,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu
165180
return &model, nil
166181
}
167182

183+
func parseTxtRecord(records *[]string) error {
184+
if records == nil {
185+
return nil
186+
}
187+
if len(*records) == 0 {
188+
return nil
189+
}
190+
191+
for idx := range *records {
192+
var err error
193+
// Based on RFC 1035 section 2.3.4, TXT Records are limited to 255 Characters.
194+
// Longer strings need to be split into multiple records
195+
(*records)[idx], err = dnsUtils.FormatTxtRecord((*records)[idx])
196+
if err != nil {
197+
return err
198+
}
199+
}
200+
201+
return nil
202+
}
203+
168204
func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiPartialUpdateRecordSetRequest {
169205
var records *[]dns.RecordPayload = nil
170206
if model.Records != nil {

internal/cmd/dns/record-set/update/update_test.go

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ var testProjectId = uuid.NewString()
2424
var testZoneId = uuid.NewString()
2525
var testRecordSetId = uuid.NewString()
2626

27+
var (
28+
text255Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo"
29+
text256Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoob"
30+
result256Characters = "\"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo\" \"b\""
31+
text4050Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo"
32+
)
33+
2734
func fixtureArgValues(mods ...func(argValues []string)) []string {
2835
argValues := []string{
2936
testRecordSetId,
@@ -78,10 +85,11 @@ func fixtureRequest(mods ...func(request *dns.ApiPartialUpdateRecordSetRequest))
7885
},
7986
Ttl: utils.Ptr(int64(3600)),
8087
})
88+
req := &request
8189
for _, mod := range mods {
82-
mod(&request)
90+
mod(req)
8391
}
84-
return request
92+
return *req
8593
}
8694

8795
func TestParseInput(t *testing.T) {
@@ -306,6 +314,71 @@ func TestParseInput(t *testing.T) {
306314
}
307315
}
308316

317+
func TestParseTxtRecord(t *testing.T) {
318+
tests := []struct {
319+
description string
320+
records *[]string
321+
expectedResult *[]string
322+
isValid bool
323+
shouldErr bool
324+
}{
325+
{
326+
description: "empty",
327+
records: nil,
328+
expectedResult: nil,
329+
isValid: true,
330+
},
331+
{
332+
description: "base",
333+
records: &[]string{"foobar"},
334+
expectedResult: &[]string{"foobar"},
335+
isValid: true,
336+
},
337+
{
338+
description: "input has length of 255 characters and should not split",
339+
records: &[]string{text255Characters},
340+
expectedResult: &[]string{text255Characters},
341+
isValid: true,
342+
},
343+
{
344+
description: "input has length 256 characters and should split",
345+
records: &[]string{text256Characters},
346+
expectedResult: &[]string{result256Characters},
347+
isValid: true,
348+
},
349+
{
350+
description: "input has length 4050 characters and should fail",
351+
records: &[]string{text4050Characters},
352+
isValid: false,
353+
},
354+
}
355+
for _, tt := range tests {
356+
t.Run(tt.description, func(t *testing.T) {
357+
err := parseTxtRecord(tt.records)
358+
if err != nil {
359+
if !tt.isValid {
360+
return
361+
}
362+
t.Fatalf("should not fail but got error: %v", err)
363+
return
364+
}
365+
if err == nil && !tt.isValid {
366+
t.Fatalf("should fail but got none")
367+
return
368+
}
369+
370+
if !tt.isValid {
371+
t.Fatalf("should fail but got none")
372+
return
373+
}
374+
diff := cmp.Diff(tt.expectedResult, tt.records)
375+
if diff != "" {
376+
t.Fatalf("Data does not match: %s", diff)
377+
}
378+
})
379+
}
380+
}
381+
309382
func TestBuildRequest(t *testing.T) {
310383
tests := []struct {
311384
description string

internal/pkg/services/dns/utils/utils.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package utils
33
import (
44
"context"
55
"fmt"
6+
"math"
67

8+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
79
"github.com/stackitcloud/stackit-sdk-go/services/dns"
810
)
911

@@ -27,3 +29,37 @@ func GetRecordSetName(ctx context.Context, apiClient DNSClient, projectId, zoneI
2729
}
2830
return *resp.Rrset.Name, nil
2931
}
32+
33+
func GetRecordSetType(ctx context.Context, apiClient DNSClient, projectId, zoneId, recordSetId string) (*string, error) {
34+
resp, err := apiClient.GetRecordSetExecute(ctx, projectId, zoneId, recordSetId)
35+
if err != nil {
36+
return utils.Ptr(""), fmt.Errorf("get DNS recordset: %w", err)
37+
}
38+
return resp.Rrset.Type, nil
39+
}
40+
41+
func FormatTxtRecord(input string) (string, error) {
42+
length := float64(len(input))
43+
if length <= 255 {
44+
return input, nil
45+
}
46+
// Max length with quotes and white spaces is 4096. Without the quotes and white spaces the max length is 4049
47+
if length > 4049 {
48+
return "", fmt.Errorf("max input length is 4049. The length of the input is %v", length)
49+
}
50+
51+
result := ""
52+
chunks := int(math.Ceil(length / 255))
53+
for i := range chunks {
54+
skip := 255 * i
55+
if i == chunks-1 {
56+
// Append the left record content
57+
result += fmt.Sprintf("%q", input[0+skip:])
58+
} else {
59+
// Add 255 characters of the record data quoted to the result
60+
result += fmt.Sprintf("%q ", input[0+skip:255+skip])
61+
}
62+
}
63+
64+
return result, nil
65+
}

0 commit comments

Comments
 (0)