Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support dns TXT records with more than 255 characters #580

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions internal/cmd/dns/record-set/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"

"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
Expand All @@ -16,8 +17,6 @@ import (
dnsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/dns/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"

"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/dns"
"github.com/stackitcloud/stackit-sdk-go/services/dns/wait"
)
Expand All @@ -31,6 +30,7 @@ const (
typeFlag = "type"

defaultType = "A"
txtType = "TXT"
)

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

if model.Type == txtType {
for idx := range model.Records {
// Based on RFC 1035 section 2.3.4, TXT Records are limited to 255 Characters
// Longer strings need to be split into multiple records
if len(model.Records[idx]) > 255 {
var err error
model.Records[idx], err = dnsUtils.FormatTxtRecord(model.Records[idx])
if err != nil {
return nil, err
}
}
}
}

if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
Expand Down
31 changes: 29 additions & 2 deletions internal/cmd/dns/record-set/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package create

import (
"context"
"fmt"
"strings"
"testing"

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

var recordTxtOver255Char = []string{
"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo",
"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo",
"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar",
}

func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
Expand Down Expand Up @@ -76,7 +84,7 @@ func fixtureRequest(mods ...func(request *dns.ApiCreateRecordSetRequest)) dns.Ap
}

func TestParseInput(t *testing.T) {
tests := []struct {
var tests = []struct {
description string
flagValues map[string]string
recordFlagValues []string
Expand Down Expand Up @@ -236,8 +244,27 @@ func TestParseInput(t *testing.T) {
model.Records = append(model.Records, "1.2.3.4", "5.6.7.8")
}),
},
}
{
description: "TXT record with > 255 characters",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[typeFlag] = txtType
flagValues[recordFlag] = strings.Join(recordTxtOver255Char, "")
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
var content string
for idx, val := range recordTxtOver255Char {
content += fmt.Sprintf("%q", val)
if idx != len(recordTxtOver255Char)-1 {
content += " "
}
}

model.Records = []string{content}
model.Type = txtType
}),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
Expand Down
36 changes: 36 additions & 0 deletions internal/cmd/dns/record-set/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
nameFlag = "name"
recordFlag = "record"
ttlFlag = "ttl"
txtType = "TXT"
)

type inputModel struct {
Expand All @@ -38,6 +39,7 @@ type inputModel struct {
Name *string
Records *[]string
TTL *int64
Type *string
}

func NewCmd(p *print.Printer) *cobra.Command {
Expand Down Expand Up @@ -76,6 +78,19 @@ func NewCmd(p *print.Printer) *cobra.Command {
recordSetLabel = model.RecordSetId
}

typeLabel, err := dnsUtils.GetRecordSetType(ctx, apiClient, model.ProjectId, model.ZoneId, model.RecordSetId)
if err != nil {
p.Debug(print.ErrorLevel, "get record set type: %v", err)
}
model.Type = typeLabel

if utils.PtrString(model.Type) == txtType {
err = parseTxtRecord(model.Records)
if err != nil {
return err
}
}

if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to update record set %s of zone %s?", recordSetLabel, zoneLabel)
err = p.PromptForConfirmation(prompt)
Expand Down Expand Up @@ -165,6 +180,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu
return &model, nil
}

func parseTxtRecord(records *[]string) error {
if records == nil {
return nil
}
if len(*records) == 0 {
return nil
}

for idx := range *records {
var err error
// Based on RFC 1035 section 2.3.4, TXT Records are limited to 255 Characters.
// Longer strings need to be split into multiple records
(*records)[idx], err = dnsUtils.FormatTxtRecord((*records)[idx])
if err != nil {
return err
}
}

return nil
}

func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiPartialUpdateRecordSetRequest {
var records *[]dns.RecordPayload = nil
if model.Records != nil {
Expand Down
77 changes: 75 additions & 2 deletions internal/cmd/dns/record-set/update/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ var testProjectId = uuid.NewString()
var testZoneId = uuid.NewString()
var testRecordSetId = uuid.NewString()

var (
text255Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo"
text256Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoob"
result256Characters = "\"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo\" \"b\""
text4050Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo"
)

func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testRecordSetId,
Expand Down Expand Up @@ -78,10 +85,11 @@ func fixtureRequest(mods ...func(request *dns.ApiPartialUpdateRecordSetRequest))
},
Ttl: utils.Ptr(int64(3600)),
})
req := &request
for _, mod := range mods {
mod(&request)
mod(req)
}
return request
return *req
}

func TestParseInput(t *testing.T) {
Expand Down Expand Up @@ -306,6 +314,71 @@ func TestParseInput(t *testing.T) {
}
}

func TestParseTxtRecord(t *testing.T) {
tests := []struct {
description string
records *[]string
expectedResult *[]string
isValid bool
shouldErr bool
}{
{
description: "empty",
records: nil,
expectedResult: nil,
isValid: true,
},
{
description: "base",
records: &[]string{"foobar"},
expectedResult: &[]string{"foobar"},
isValid: true,
},
{
description: "input has length of 255 characters and should not split",
records: &[]string{text255Characters},
expectedResult: &[]string{text255Characters},
isValid: true,
},
{
description: "input has length 256 characters and should split",
records: &[]string{text256Characters},
expectedResult: &[]string{result256Characters},
isValid: true,
},
{
description: "input has length 4050 characters and should fail",
records: &[]string{text4050Characters},
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := parseTxtRecord(tt.records)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("should not fail but got error: %v", err)
return
}
if err == nil && !tt.isValid {
t.Fatalf("should fail but got none")
return
}

if !tt.isValid {
t.Fatalf("should fail but got none")
return
}
diff := cmp.Diff(tt.expectedResult, tt.records)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}

func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
Expand Down
36 changes: 36 additions & 0 deletions internal/pkg/services/dns/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package utils
import (
"context"
"fmt"
"math"

"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/dns"
)

Expand All @@ -27,3 +29,37 @@ func GetRecordSetName(ctx context.Context, apiClient DNSClient, projectId, zoneI
}
return *resp.Rrset.Name, nil
}

func GetRecordSetType(ctx context.Context, apiClient DNSClient, projectId, zoneId, recordSetId string) (*string, error) {
resp, err := apiClient.GetRecordSetExecute(ctx, projectId, zoneId, recordSetId)
if err != nil {
return utils.Ptr(""), fmt.Errorf("get DNS recordset: %w", err)
}
return resp.Rrset.Type, nil
}

func FormatTxtRecord(input string) (string, error) {
length := float64(len(input))
if length <= 255 {
return input, nil
}
// Max length with quotes and white spaces is 4096. Without the quotes and white spaces the max length is 4049
if length > 4049 {
return "", fmt.Errorf("max input length is 4049. The length of the input is %v", length)
}

result := ""
chunks := int(math.Ceil(length / 255))
for i := range chunks {
skip := 255 * i
if i == chunks-1 {
// Append the left record content
result += fmt.Sprintf("%q", input[0+skip:])
} else {
// Add 255 characters of the record data quoted to the result
result += fmt.Sprintf("%q ", input[0+skip:255+skip])
}
}

return result, nil
}
Loading