55 "errors"
66 "fmt"
77 "log"
8- "strconv "
8+ "strings "
99 "time"
1010
1111 "github.com/aws/aws-sdk-go-v2/aws"
@@ -49,12 +49,105 @@ func (p *Provider) init(ctx context.Context) {
4949 cfg , err := config .LoadDefaultConfig (ctx , opts ... )
5050
5151 if err != nil {
52- log .Fatal ( err )
52+ log .Fatalf ( "route53: unable to load AWS SDK config, %v" , err )
5353 }
5454
5555 p .client = r53 .NewFromConfig (cfg )
5656}
5757
58+ func chunkString (s string , chunkSize int ) []string {
59+ var chunks []string
60+ for i := 0 ; i < len (s ); i += chunkSize {
61+ end := i + chunkSize
62+ if end > len (s ) {
63+ end = len (s )
64+ }
65+ chunks = append (chunks , s [i :end ])
66+ }
67+ return chunks
68+ }
69+
70+ func parseRecordSet (set types.ResourceRecordSet ) []libdns.Record {
71+ records := make ([]libdns.Record , 0 )
72+
73+ // Route53 returns TXT & SPF records with quotes around them.
74+ // https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#TXTFormat
75+ var ttl int64
76+ if set .TTL != nil {
77+ ttl = * set .TTL
78+ }
79+
80+ rtype := string (set .Type )
81+ for _ , record := range set .ResourceRecords {
82+ value := * record .Value
83+ switch rtype {
84+ case "TXT" , "SPF" :
85+ rows := strings .Split (value , "\n " )
86+ for i , row := range rows {
87+ parts := strings .Split (row , `" "` )
88+ if len (parts ) > 0 {
89+ parts [0 ] = strings .TrimPrefix (parts [0 ], `"` )
90+ parts [len (parts )- 1 ] = strings .TrimSuffix (parts [len (parts )- 1 ], `"` )
91+ }
92+
93+ // Join parts
94+ row = strings .Join (parts , "" )
95+ row = unquote (row )
96+ rows [i ] = row
97+
98+ records = append (records , libdns.Record {
99+ Name : * set .Name ,
100+ Value : row ,
101+ Type : rtype ,
102+ TTL : time .Duration (ttl ) * time .Second ,
103+ })
104+ }
105+ default :
106+ records = append (records , libdns.Record {
107+ Name : * set .Name ,
108+ Value : value ,
109+ Type : rtype ,
110+ TTL : time .Duration (ttl ) * time .Second ,
111+ })
112+ }
113+
114+ }
115+
116+ return records
117+ }
118+
119+ func marshalRecord (record libdns.Record ) []types.ResourceRecord {
120+ resourceRecords := make ([]types.ResourceRecord , 0 )
121+
122+ // Route53 requires TXT & SPF records to be quoted.
123+ // https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#TXTFormat
124+ switch record .Type {
125+ case "TXT" , "SPF" :
126+ strs := make ([]string , 0 )
127+ if len (record .Value ) > 255 {
128+ strs = append (strs , chunkString (record .Value , 255 )... )
129+ } else {
130+ strs = append (strs , record .Value )
131+ }
132+
133+ // Quote strings
134+ for i , str := range strs {
135+ strs [i ] = quote (str )
136+ }
137+
138+ // Finally join chunks with spaces
139+ resourceRecords = append (resourceRecords , types.ResourceRecord {
140+ Value : aws .String (strings .Join (strs , " " )),
141+ })
142+ default :
143+ resourceRecords = append (resourceRecords , types.ResourceRecord {
144+ Value : aws .String (record .Value ),
145+ })
146+ }
147+
148+ return resourceRecords
149+ }
150+
58151func (p * Provider ) getRecords (ctx context.Context , zoneID string , zone string ) ([]libdns.Record , error ) {
59152 getRecordsInput := & r53.ListResourceRecordSetsInput {
60153 HostedZoneId : aws .String (zoneID ),
@@ -79,6 +172,10 @@ func (p *Provider) getRecords(ctx context.Context, zoneID string, zone string) (
79172 }
80173
81174 recordSets = append (recordSets , getRecordResult .ResourceRecordSets ... )
175+ for _ , s := range recordSets {
176+ records = append (records , parseRecordSet (s )... )
177+ }
178+
82179 if getRecordResult .IsTruncated {
83180 getRecordsInput .StartRecordName = getRecordResult .NextRecordName
84181 getRecordsInput .StartRecordType = getRecordResult .NextRecordType
@@ -88,31 +185,6 @@ func (p *Provider) getRecords(ctx context.Context, zoneID string, zone string) (
88185 }
89186 }
90187
91- for _ , rrset := range recordSets {
92- for _ , rrsetRecord := range rrset .ResourceRecords {
93- rtype := rrset .Type
94- value := * rrsetRecord .Value
95- // Route53 returns TXT & SPF records with quotes around them.
96- // https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#TXTFormat
97- switch rtype {
98- case types .RRTypeTxt , types .RRTypeSpf :
99- var err error
100- value , err = strconv .Unquote (value )
101- if err != nil {
102- return records , fmt .Errorf ("Error unquoting TXT/SPF record: %s" , err )
103- }
104- }
105- record := libdns.Record {
106- Name : * rrset .Name ,
107- Value : value ,
108- Type : string (rtype ),
109- TTL : time .Duration (* rrset .TTL ) * time .Second ,
110- }
111-
112- records = append (records , record )
113- }
114- }
115-
116188 return records , nil
117189}
118190
@@ -170,24 +242,19 @@ func (p *Provider) createRecord(ctx context.Context, zoneID string, record libdn
170242 switch record .Type {
171243 case "TXT" :
172244 return p .updateRecord (ctx , zoneID , record , zone )
173- case "SPF" :
174- record .Value = strconv .Quote (record .Value )
175245 }
176246
247+ resourceRecords := marshalRecord (record )
177248 createInput := & r53.ChangeResourceRecordSetsInput {
178249 ChangeBatch : & types.ChangeBatch {
179250 Changes : []types.Change {
180251 {
181252 Action : types .ChangeActionCreate ,
182253 ResourceRecordSet : & types.ResourceRecordSet {
183- Name : aws .String (libdns .AbsoluteName (record .Name , zone )),
184- ResourceRecords : []types.ResourceRecord {
185- {
186- Value : aws .String (record .Value ),
187- },
188- },
189- TTL : aws .Int64 (int64 (record .TTL .Seconds ())),
190- Type : types .RRType (record .Type ),
254+ Name : aws .String (libdns .AbsoluteName (record .Name , zone )),
255+ ResourceRecords : resourceRecords ,
256+ TTL : aws .Int64 (int64 (record .TTL .Seconds ())),
257+ Type : types .RRType (record .Type ),
191258 },
192259 },
193260 },
@@ -206,26 +273,19 @@ func (p *Provider) createRecord(ctx context.Context, zoneID string, record libdn
206273func (p * Provider ) updateRecord (ctx context.Context , zoneID string , record libdns.Record , zone string ) (libdns.Record , error ) {
207274 resourceRecords := make ([]types.ResourceRecord , 0 )
208275 // AWS Route53 TXT record value must be enclosed in quotation marks on update
209- switch record .Type {
210- case "SPF" , "TXT" :
211- resourceRecords = append (resourceRecords , types.ResourceRecord {
212- Value : aws .String (strconv .Quote (record .Value )),
213- })
214- }
215276 if record .Type == "TXT" {
216277 txtRecords , err := p .getTxtRecordsFor (ctx , zoneID , zone , record .Name )
217278 if err != nil {
218279 return record , err
219280 }
220281 for _ , r := range txtRecords {
221282 if record .Value != r .Value {
222- resourceRecords = append (resourceRecords , types.ResourceRecord {
223- Value : aws .String (strconv .Quote (r .Value )),
224- })
283+ resourceRecords = append (resourceRecords , marshalRecord (r )... )
225284 }
226285 }
227286 }
228287
288+ resourceRecords = append (resourceRecords , marshalRecord (record )... )
229289 updateInput := & r53.ChangeResourceRecordSetsInput {
230290 ChangeBatch : & types.ChangeBatch {
231291 Changes : []types.Change {
@@ -255,28 +315,24 @@ func (p *Provider) deleteRecord(ctx context.Context, zoneID string, record libdn
255315 action := types .ChangeActionDelete
256316 resourceRecords := make ([]types.ResourceRecord , 0 )
257317 // AWS Route53 TXT record value must be enclosed in quotation marks on update
258- switch record .Type {
259- case "SPF" , "TXT" :
260- resourceRecords = append (resourceRecords , types.ResourceRecord {
261- Value : aws .String (strconv .Quote (record .Value )),
262- })
263- }
264318 if record .Type == "TXT" {
265319 txtRecords , err := p .getTxtRecordsFor (ctx , zoneID , zone , record .Name )
266320 if err != nil {
267321 return record , err
268322 }
323+
269324 switch {
270- case len (txtRecords ) > 0 && txtRecords [0 ].Value != record .Value ,
271- len (txtRecords ) > 1 :
325+ // If there is only one record, we can delete the entire record set.
326+ case len (txtRecords ) == 1 :
327+ resourceRecords = append (resourceRecords , marshalRecord (record )... )
328+ // If there are multiple records, we need to upsert the remaining records.
329+ case len (txtRecords ) > 1 :
272330 action = types .ChangeActionUpsert
273331 resourceRecords = make ([]types.ResourceRecord , 0 )
274- }
275- for _ , r := range txtRecords {
276- if record .Value != r .Value {
277- resourceRecords = append (resourceRecords , types.ResourceRecord {
278- Value : aws .String (strconv .Quote (r .Value )),
279- })
332+ for _ , r := range txtRecords {
333+ if record .Value != r .Value {
334+ resourceRecords = append (resourceRecords , marshalRecord (r )... )
335+ }
280336 }
281337 }
282338 }
0 commit comments