From 99e41783b06d2a57a3cc04ed26dd8fca207c8366 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 7 Sep 2022 09:40:52 +0300 Subject: [PATCH 1/7] fix: unquote TXT & SPF records --- client.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 116b853..738945e 100644 --- a/client.go +++ b/client.go @@ -90,10 +90,22 @@ func (p *Provider) getRecords(ctx context.Context, zoneID string, zone string) ( for _, rrset := range recordSets { for _, rrsetRecord := range rrset.ResourceRecords { + rtype := rrset.Type + value := *rrsetRecord.Value + // Route53 returns TXT & SPF records with quotes around them. + // https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#TXTFormat + switch rtype { + case types.RRTypeTxt, types.RRTypeSpf: + var err error + value, err = strconv.Unquote(value) + if err != nil { + return records, fmt.Errorf("Error unquoting TXT/SPF record: %s", err) + } + } record := libdns.Record{ Name: libdns.AbsoluteName(*rrset.Name, zone), - Value: *rrsetRecord.Value, - Type: string(rrset.Type), + Value: value, + Type: string(rtype), TTL: time.Duration(*rrset.TTL) * time.Second, } From 8daa364a21b5366fcc3117c6be212c4e968c225e Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 7 Sep 2022 10:06:53 +0300 Subject: [PATCH 2/7] fix: records name on get --- client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.go b/client.go index 738945e..7be83ab 100644 --- a/client.go +++ b/client.go @@ -103,7 +103,7 @@ func (p *Provider) getRecords(ctx context.Context, zoneID string, zone string) ( } } record := libdns.Record{ - Name: libdns.AbsoluteName(*rrset.Name, zone), + Name: *rrset.Name, Value: value, Type: string(rtype), TTL: time.Duration(*rrset.TTL) * time.Second, From 77dff685665609384f335bf265d0c6ed3a280726 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 7 Sep 2022 10:16:38 +0300 Subject: [PATCH 3/7] fix: quote TXT & SPF records --- client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 7be83ab..949519e 100644 --- a/client.go +++ b/client.go @@ -146,7 +146,7 @@ func (p *Provider) getZoneID(ctx context.Context, zoneName string) (string, erro func (p *Provider) createRecord(ctx context.Context, zoneID string, record libdns.Record, zone string) (libdns.Record, error) { // AWS Route53 TXT record value must be enclosed in quotation marks on create - if record.Type == "TXT" { + if record.Type == "TXT" || record.Type == "SPF" { record.Value = strconv.Quote(record.Value) } @@ -181,7 +181,7 @@ func (p *Provider) createRecord(ctx context.Context, zoneID string, record libdn func (p *Provider) updateRecord(ctx context.Context, zoneID string, record libdns.Record, zone string) (libdns.Record, error) { // AWS Route53 TXT record value must be enclosed in quotation marks on update - if record.Type == "TXT" { + if record.Type == "TXT" || record.Type == "SPF" { record.Value = strconv.Quote(record.Value) } From bf40a144470de4507efbaaef75649f2e18c7f260 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 7 Sep 2022 12:00:58 +0300 Subject: [PATCH 4/7] fix: multiple TXT records catches: * record TTL will be set to the latest TXT record upserted * TXT records will always be upserted i.e. it won't fail if TXT record already exists --- client.go | 66 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/client.go b/client.go index 949519e..b607442 100644 --- a/client.go +++ b/client.go @@ -146,7 +146,10 @@ func (p *Provider) getZoneID(ctx context.Context, zoneName string) (string, erro func (p *Provider) createRecord(ctx context.Context, zoneID string, record libdns.Record, zone string) (libdns.Record, error) { // AWS Route53 TXT record value must be enclosed in quotation marks on create - if record.Type == "TXT" || record.Type == "SPF" { + switch record.Type { + case "TXT": + return p.updateRecord(ctx, zoneID, record, zone) + case "SPF": record.Value = strconv.Quote(record.Value) } @@ -180,9 +183,26 @@ func (p *Provider) createRecord(ctx context.Context, zoneID string, record libdn } func (p *Provider) updateRecord(ctx context.Context, zoneID string, record libdns.Record, zone string) (libdns.Record, error) { + resourceRecords := make([]types.ResourceRecord, 0) // AWS Route53 TXT record value must be enclosed in quotation marks on update - if record.Type == "TXT" || record.Type == "SPF" { - record.Value = strconv.Quote(record.Value) + switch record.Type { + case "SPF", "TXT": + resourceRecords = append(resourceRecords, types.ResourceRecord{ + Value: aws.String(strconv.Quote(record.Value)), + }) + } + if record.Type == "TXT" { + txtRecords, err := p.getTxtRecordsFor(ctx, zoneID, zone, record.Name) + if err != nil { + return record, err + } + for _, r := range txtRecords { + if record.Value != r.Value { + resourceRecords = append(resourceRecords, types.ResourceRecord{ + Value: aws.String(strconv.Quote(r.Value)), + }) + } + } } updateInput := &r53.ChangeResourceRecordSetsInput{ @@ -191,14 +211,10 @@ func (p *Provider) updateRecord(ctx context.Context, zoneID string, record libdn { Action: types.ChangeActionUpsert, ResourceRecordSet: &types.ResourceRecordSet{ - Name: aws.String(libdns.AbsoluteName(record.Name, zone)), - ResourceRecords: []types.ResourceRecord{ - { - Value: aws.String(record.Value), - }, - }, - TTL: aws.Int64(int64(record.TTL.Seconds())), - Type: types.RRType(record.Type), + Name: aws.String(libdns.AbsoluteName(record.Name, zone)), + ResourceRecords: resourceRecords, + TTL: aws.Int64(int64(record.TTL.Seconds())), + Type: types.RRType(record.Type), }, }, }, @@ -280,3 +296,31 @@ func (p *Provider) applyChange(ctx context.Context, input *r53.ChangeResourceRec return nil } + +func (p *Provider) getTxtRecords(ctx context.Context, zoneID string, zone string) ([]libdns.Record, error) { + txtRecords := make([]libdns.Record, 0) + records, err := p.getRecords(ctx, zoneID, zone) + if err != nil { + return nil, err + } + for _, r := range records { + if r.Type == "TXT" { + txtRecords = append(txtRecords, r) + } + } + return txtRecords, nil +} + +func (p *Provider) getTxtRecordsFor(ctx context.Context, zoneID string, zone string, name string) ([]libdns.Record, error) { + txtRecords, err := p.getTxtRecords(ctx, zoneID, zone) + if err != nil { + return nil, err + } + records := make([]libdns.Record, 0) + for _, r := range txtRecords { + if libdns.AbsoluteName(name, zone) == r.Name { + records = append(records, r) + } + } + return records, nil +} From f5fb954748cf2834a7b4c92b8132a7c8e9ffcf2f Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 4 Oct 2022 14:13:04 -0400 Subject: [PATCH 5/7] fix: return errors as is --- client.go | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/client.go b/client.go index b607442..fcd18c4 100644 --- a/client.go +++ b/client.go @@ -263,21 +263,7 @@ func (p *Provider) deleteRecord(ctx context.Context, zoneID string, record libdn func (p *Provider) applyChange(ctx context.Context, input *r53.ChangeResourceRecordSetsInput) error { changeResult, err := p.client.ChangeResourceRecordSets(ctx, input) if err != nil { - var nshze *types.NoSuchHostedZone - var icbe *types.InvalidChangeBatch - var iie *types.InvalidInput - var prnce *types.PriorRequestNotComplete - if errors.As(err, &nshze) { - return fmt.Errorf("NoSuchHostedZone: %s", err) - } else if errors.As(err, &icbe) { - return fmt.Errorf("InvalidChangeBatch: %s", err) - } else if errors.As(err, &iie) { - return fmt.Errorf("InvalidInput: %s", err) - } else if errors.As(err, &prnce) { - return fmt.Errorf("PriorRequestNotComplete: %s", err) - } else { - return err - } + return err } // Waiting for propagation if it's set in the provider config. From 44e24f4249a43f632b4a97acf61c1485c1bfb96c Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 4 Oct 2022 14:13:35 -0400 Subject: [PATCH 6/7] fix: delete txt records don't error on txt delete not found record error --- client.go | 47 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/client.go b/client.go index fcd18c4..ac331da 100644 --- a/client.go +++ b/client.go @@ -231,20 +231,45 @@ func (p *Provider) updateRecord(ctx context.Context, zoneID string, record libdn } func (p *Provider) deleteRecord(ctx context.Context, zoneID string, record libdns.Record, zone string) (libdns.Record, error) { + action := types.ChangeActionDelete + resourceRecords := make([]types.ResourceRecord, 0) + // AWS Route53 TXT record value must be enclosed in quotation marks on update + switch record.Type { + case "SPF", "TXT": + resourceRecords = append(resourceRecords, types.ResourceRecord{ + Value: aws.String(strconv.Quote(record.Value)), + }) + } + if record.Type == "TXT" { + txtRecords, err := p.getTxtRecordsFor(ctx, zoneID, zone, record.Name) + if err != nil { + return record, err + } + switch { + case len(txtRecords) > 0 && txtRecords[0].Value != record.Value, + len(txtRecords) > 1: + action = types.ChangeActionUpsert + resourceRecords = make([]types.ResourceRecord, 0) + } + for _, r := range txtRecords { + if record.Value != r.Value { + resourceRecords = append(resourceRecords, types.ResourceRecord{ + Value: aws.String(strconv.Quote(r.Value)), + }) + } + } + } + deleteInput := &r53.ChangeResourceRecordSetsInput{ ChangeBatch: &types.ChangeBatch{ Changes: []types.Change{ { - Action: types.ChangeActionDelete, + Action: action, ResourceRecordSet: &types.ResourceRecordSet{ - Name: aws.String(libdns.AbsoluteName(record.Name, zone)), - ResourceRecords: []types.ResourceRecord{ - { - Value: aws.String(record.Value), - }, - }, - TTL: aws.Int64(int64(record.TTL.Seconds())), - Type: types.RRType(record.Type), + Name: aws.String(libdns.AbsoluteName(record.Name, zone)), + ResourceRecords: resourceRecords, + TTL: aws.Int64(int64(record.TTL.Seconds())), + Type: types.RRType(record.Type), }, }, }, @@ -254,6 +279,10 @@ func (p *Provider) deleteRecord(ctx context.Context, zoneID string, record libdn err := p.applyChange(ctx, deleteInput) if err != nil { + var nfe *types.InvalidChangeBatch + if record.Type == "TXT" && errors.As(err, &nfe) { + return record, nil + } return record, err } From 6d98c517819c40edc0de0ff13ea8f23397f46fe1 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 4 Oct 2022 14:14:21 -0400 Subject: [PATCH 7/7] chore: add examples --- examples/createTxt/main.go | 34 +++++++++++++++++++++++++++++++++ examples/go.mod | 24 +++++++++++++++++++++++ examples/go.sum | 39 ++++++++++++++++++++++++++++++++++++++ examples/list/main.go | 19 +++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 examples/createTxt/main.go create mode 100644 examples/go.mod create mode 100644 examples/go.sum create mode 100644 examples/list/main.go diff --git a/examples/createTxt/main.go b/examples/createTxt/main.go new file mode 100644 index 0000000..7d5ab46 --- /dev/null +++ b/examples/createTxt/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "context" + "log" + + "github.com/libdns/libdns" + "github.com/libdns/route53" +) + +func main() { + p := &route53.Provider{} + ctx := context.Background() + _, err := p.AppendRecords(ctx, "charm.sh.", []libdns.Record{ + { + Name: "abctxt", + Value: `This string includes "quotation marks".`, + Type: "TXT", + }, + { + Name: "abctxt", + Value: `The last character in this string is an accented e specified in octal format: \351`, + Type: "TXT", + }, + { + Name: "abctxt", + Value: "v=spf1 ip4:192.168.0.1/16 -all", + Type: "TXT", + }, + }) + if err != nil { + log.Fatalln(err) + } +} diff --git a/examples/go.mod b/examples/go.mod new file mode 100644 index 0000000..bc84fa6 --- /dev/null +++ b/examples/go.mod @@ -0,0 +1,24 @@ +module github.com/libdns/route53/examples + +go 1.19 + +replace github.com/libdns/route53 => ../ + +require ( + github.com/libdns/libdns v0.2.1 + github.com/libdns/route53 v1.2.2 +) + +require ( + github.com/aws/aws-sdk-go-v2 v1.10.0 // indirect + github.com/aws/aws-sdk-go-v2/config v1.9.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.5.0 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.2.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.4.0 // indirect + github.com/aws/aws-sdk-go-v2/service/route53 v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.5.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.8.0 // indirect + github.com/aws/smithy-go v1.8.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect +) diff --git a/examples/go.sum b/examples/go.sum new file mode 100644 index 0000000..54962a0 --- /dev/null +++ b/examples/go.sum @@ -0,0 +1,39 @@ +github.com/aws/aws-sdk-go-v2 v1.10.0 h1:+dCJ5W2HiZNa4UtaIc5ljKNulm0dK0vS5dxb5LdDOAA= +github.com/aws/aws-sdk-go-v2 v1.10.0/go.mod h1:U/EyyVvKtzmFeQQcca7eBotKdlpcP2zzU6bXBYcf7CE= +github.com/aws/aws-sdk-go-v2/config v1.9.0 h1:SkREVSwi+J8MSdjhJ96jijZm5ZDNleI0E4hHCNivh7s= +github.com/aws/aws-sdk-go-v2/config v1.9.0/go.mod h1:qhK5NNSgo9/nOSMu3HyE60WHXZTWTHTgd5qtIF44vOQ= +github.com/aws/aws-sdk-go-v2/credentials v1.5.0 h1:r6470olsn2qyOe2aLzK6q+wfO3dzNcMujRT3gqBgBB8= +github.com/aws/aws-sdk-go-v2/credentials v1.5.0/go.mod h1:kvqTkpzQmzri9PbsiTY+LvwFzM0gY19emlAWwBOJMb0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.7.0 h1:FKaqk7geL3oIqSwGJt5SWUKj8uJ+qLZNqlBuqq6sFyA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.7.0/go.mod h1:KqEkRkxm/+1Pd/rENRNbQpfblDBYeg5HDSqjB6ks8hA= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.5 h1:zPxLGWALExNepElO0gYgoqsbqTlt4ZCrhZ7XlfJ+Qlw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.5/go.mod h1:6ZBTuDmvpCOD4Sf1i2/I3PgftlEcDGgvi8ocq64oQEg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.4.0 h1:/T5wKsw/po118HEDvnSE8YU7TESxvZbYM2rnn+Oi7Kk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.4.0/go.mod h1:X5/JuOxPLU/ogICgDTtnpfaQzdQJO0yKDcpoxWLLJ8Y= +github.com/aws/aws-sdk-go-v2/service/route53 v1.12.0 h1:XNmW6Z/l4NL/Glz76gqAb6WOgdSYC2a1T0YBBEHfQ58= +github.com/aws/aws-sdk-go-v2/service/route53 v1.12.0/go.mod h1:LbPVLMeOEGLIW54yuMayW70DcTtsb+17ekL5j48deF4= +github.com/aws/aws-sdk-go-v2/service/sso v1.5.0 h1:VnrCAJTp1bDxU79UuW/D4z7bwZ7xOc7JjDKpqXL/m04= +github.com/aws/aws-sdk-go-v2/service/sso v1.5.0/go.mod h1:GsqaJOJeOfeYD88/2vHWKXegvDRofDqWwC5i48A2kgs= +github.com/aws/aws-sdk-go-v2/service/sts v1.8.0 h1:7N7RsEVvUcvEg7jrWKU5AnSi4/6b6eY9+wG1g6W4ExE= +github.com/aws/aws-sdk-go-v2/service/sts v1.8.0/go.mod h1:dOlm91B439le5y1vtPCk5yJtbx3RdT3hRGYRY8TYKvQ= +github.com/aws/smithy-go v1.8.1 h1:9Y6qxtzgEODaLNGN+oN2QvcHvKUe4jsH8w4M+8LXzGk= +github.com/aws/smithy-go v1.8.1/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= +github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/examples/list/main.go b/examples/list/main.go new file mode 100644 index 0000000..354ca90 --- /dev/null +++ b/examples/list/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "context" + "log" + + "github.com/libdns/route53" +) + +func main() { + p := &route53.Provider{} + rs, err := p.GetRecords(context.Background(), "example.com.") + if err != nil { + log.Fatalln(err) + } + for _, r := range rs { + log.Printf("type: %v, value: %v, ttl: %v", r.Type, r.Value, r.TTL) + } +}