@@ -27,7 +27,6 @@ import (
27
27
"sort"
28
28
"strconv"
29
29
"strings"
30
- "time"
31
30
32
31
cloudflare "github.com/cloudflare/cloudflare-go"
33
32
log "github.com/sirupsen/logrus"
@@ -74,6 +73,11 @@ type CustomHostnameIndex struct {
74
73
75
74
type CustomHostnamesMap map [CustomHostnameIndex ]cloudflare.CustomHostname
76
75
76
+ type DataLocalizationRegionalHostnameChange struct {
77
+ Action string
78
+ cloudflare.RegionalHostname
79
+ }
80
+
77
81
var recordTypeProxyNotSupported = map [string ]bool {
78
82
"LOC" : true ,
79
83
"MX" : true ,
@@ -94,6 +98,12 @@ var recordTypeCustomHostnameSupported = map[string]bool{
94
98
"CNAME" : true ,
95
99
}
96
100
101
+ var recordTypeRegionalHostnameSupported = map [string ]bool {
102
+ "A" : true ,
103
+ "AAAA" : true ,
104
+ "CNAME" : true ,
105
+ }
106
+
97
107
// cloudFlareDNS is the subset of the CloudFlare API that we actually use. Add methods as required. Signatures must match exactly.
98
108
type cloudFlareDNS interface {
99
109
UserDetails (ctx context.Context ) (cloudflare.User , error )
@@ -105,7 +115,9 @@ type cloudFlareDNS interface {
105
115
CreateDNSRecord (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.CreateDNSRecordParams ) (cloudflare.DNSRecord , error )
106
116
DeleteDNSRecord (ctx context.Context , rc * cloudflare.ResourceContainer , recordID string ) error
107
117
UpdateDNSRecord (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.UpdateDNSRecordParams ) error
118
+ CreateDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.CreateDataLocalizationRegionalHostnameParams ) error
108
119
UpdateDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.UpdateDataLocalizationRegionalHostnameParams ) error
120
+ DeleteDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , hostname string ) error
109
121
CustomHostnames (ctx context.Context , zoneID string , page int , filter cloudflare.CustomHostname ) ([]cloudflare.CustomHostname , cloudflare.ResultInfo , error )
110
122
DeleteCustomHostname (ctx context.Context , zoneID string , customHostnameID string ) error
111
123
CreateCustomHostname (ctx context.Context , zoneID string , ch cloudflare.CustomHostname ) (* cloudflare.CustomHostnameResponse , error )
@@ -140,11 +152,20 @@ func (z zoneService) UpdateDNSRecord(ctx context.Context, rc *cloudflare.Resourc
140
152
return err
141
153
}
142
154
155
+ func (z zoneService ) CreateDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.CreateDataLocalizationRegionalHostnameParams ) error {
156
+ _ , err := z .service .CreateDataLocalizationRegionalHostname (ctx , rc , rp )
157
+ return err
158
+ }
159
+
143
160
func (z zoneService ) UpdateDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.UpdateDataLocalizationRegionalHostnameParams ) error {
144
161
_ , err := z .service .UpdateDataLocalizationRegionalHostname (ctx , rc , rp )
145
162
return err
146
163
}
147
164
165
+ func (z zoneService ) DeleteDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , hostname string ) error {
166
+ return z .service .DeleteDataLocalizationRegionalHostname (ctx , rc , hostname )
167
+ }
168
+
148
169
func (z zoneService ) DeleteDNSRecord (ctx context.Context , rc * cloudflare.ResourceContainer , recordID string ) error {
149
170
return z .service .DeleteDNSRecord (ctx , rc , recordID )
150
171
}
@@ -208,11 +229,19 @@ func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams
208
229
}
209
230
}
210
231
232
+ // createDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
233
+ func createDataLocalizationRegionalHostnameParams (rhc DataLocalizationRegionalHostnameChange ) cloudflare.CreateDataLocalizationRegionalHostnameParams {
234
+ return cloudflare.CreateDataLocalizationRegionalHostnameParams {
235
+ Hostname : rhc .Hostname ,
236
+ RegionKey : rhc .RegionKey ,
237
+ }
238
+ }
239
+
211
240
// updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
212
- func updateDataLocalizationRegionalHostnameParams (cfc cloudFlareChange ) cloudflare.UpdateDataLocalizationRegionalHostnameParams {
241
+ func updateDataLocalizationRegionalHostnameParams (rhc DataLocalizationRegionalHostnameChange ) cloudflare.UpdateDataLocalizationRegionalHostnameParams {
213
242
return cloudflare.UpdateDataLocalizationRegionalHostnameParams {
214
- Hostname : cfc . RegionalHostname .Hostname ,
215
- RegionKey : cfc . RegionalHostname .RegionKey ,
243
+ Hostname : rhc .Hostname ,
244
+ RegionKey : rhc .RegionKey ,
216
245
}
217
246
}
218
247
@@ -466,6 +495,129 @@ func (p *CloudFlareProvider) submitCustomHostnameChanges(ctx context.Context, zo
466
495
return ! failedChange
467
496
}
468
497
498
+ // submitDataLocalizationRegionalHostnameChanges applies a set of data localization regional hostname changes, returns false if it fails
499
+ func (p * CloudFlareProvider ) submitDataLocalizationRegionalHostnameChanges (ctx context.Context , changes []DataLocalizationRegionalHostnameChange , resourceContainer * cloudflare.ResourceContainer ) bool {
500
+ failedChange := false
501
+
502
+ for _ , change := range changes {
503
+ logFields := log.Fields {
504
+ "hostname" : change .Hostname ,
505
+ "region_key" : change .RegionKey ,
506
+ "action" : change .Action ,
507
+ "zone" : resourceContainer .Identifier ,
508
+ }
509
+ log .WithFields (logFields ).Info ("Changing regional hostname" )
510
+ switch change .Action {
511
+ case cloudFlareCreate :
512
+ log .WithFields (logFields ).Debug ("Creating regional hostname" )
513
+ if p .DryRun {
514
+ continue
515
+ }
516
+ regionalHostnameParam := createDataLocalizationRegionalHostnameParams (change )
517
+ err := p .Client .CreateDataLocalizationRegionalHostname (ctx , resourceContainer , regionalHostnameParam )
518
+ if err != nil {
519
+ var apiErr * cloudflare.Error
520
+ if errors .As (err , & apiErr ) && apiErr .StatusCode == http .StatusConflict {
521
+ log .WithFields (logFields ).Debug ("Regional hostname already exists, updating instead" )
522
+ params := updateDataLocalizationRegionalHostnameParams (change )
523
+ err := p .Client .UpdateDataLocalizationRegionalHostname (ctx , resourceContainer , params )
524
+ if err != nil {
525
+ failedChange = true
526
+ log .WithFields (logFields ).Errorf ("failed to update regional hostname: %v" , err )
527
+ }
528
+ continue
529
+ }
530
+ failedChange = true
531
+ log .WithFields (logFields ).Errorf ("failed to create regional hostname: %v" , err )
532
+ }
533
+ case cloudFlareUpdate :
534
+ log .WithFields (logFields ).Debug ("Updating regional hostname" )
535
+ if p .DryRun {
536
+ continue
537
+ }
538
+ regionalHostnameParam := updateDataLocalizationRegionalHostnameParams (change )
539
+ err := p .Client .UpdateDataLocalizationRegionalHostname (ctx , resourceContainer , regionalHostnameParam )
540
+ if err != nil {
541
+ var apiErr * cloudflare.Error
542
+ if errors .As (err , & apiErr ) && apiErr .StatusCode == http .StatusNotFound {
543
+ log .WithFields (logFields ).Debug ("Regional hostname not does not exists, creating instead" )
544
+ params := createDataLocalizationRegionalHostnameParams (change )
545
+ err := p .Client .CreateDataLocalizationRegionalHostname (ctx , resourceContainer , params )
546
+ if err != nil {
547
+ failedChange = true
548
+ log .WithFields (logFields ).Errorf ("failed to create regional hostname: %v" , err )
549
+ }
550
+ continue
551
+ }
552
+ failedChange = true
553
+ log .WithFields (logFields ).Errorf ("failed to update regional hostname: %v" , err )
554
+ }
555
+ case cloudFlareDelete :
556
+ log .WithFields (logFields ).Debug ("Deleting regional hostname" )
557
+ if p .DryRun {
558
+ continue
559
+ }
560
+ err := p .Client .DeleteDataLocalizationRegionalHostname (ctx , resourceContainer , change .Hostname )
561
+ if err != nil {
562
+ var apiErr * cloudflare.Error
563
+ if errors .As (err , & apiErr ) && apiErr .StatusCode == http .StatusNotFound {
564
+ log .WithFields (logFields ).Debug ("Regional hostname does not exists, nothing to do" )
565
+ continue
566
+ }
567
+ failedChange = true
568
+ log .WithFields (logFields ).Errorf ("failed to delete regional hostname: %v" , err )
569
+ }
570
+ }
571
+ }
572
+
573
+ return ! failedChange
574
+ }
575
+
576
+ // dataLocalizationRegionalHostnamesChanges processes a slice of cloudFlare changes and consolidates them
577
+ // into a list of data localization regional hostname changes.
578
+ // returns nil if no changes are needed
579
+ func dataLocalizationRegionalHostnamesChanges (changes []* cloudFlareChange ) ([]DataLocalizationRegionalHostnameChange , error ) {
580
+ regionalHostnameChanges := make (map [string ]DataLocalizationRegionalHostnameChange )
581
+ for _ , change := range changes {
582
+ if change .RegionalHostname .Hostname == "" {
583
+ continue
584
+ }
585
+ if change .RegionalHostname .RegionKey == "" {
586
+ return nil , fmt .Errorf ("region key is empty for regional hostname %q" , change .RegionalHostname .Hostname )
587
+ }
588
+ regionalHostname , ok := regionalHostnameChanges [change .RegionalHostname .Hostname ]
589
+ switch change .Action {
590
+ case cloudFlareCreate , cloudFlareUpdate :
591
+ if ! ok {
592
+ regionalHostnameChanges [change .RegionalHostname .Hostname ] = DataLocalizationRegionalHostnameChange {
593
+ Action : change .Action ,
594
+ RegionalHostname : change .RegionalHostname ,
595
+ }
596
+ continue
597
+ }
598
+ if regionalHostname .RegionKey != change .RegionalHostname .RegionKey {
599
+ return nil , fmt .Errorf ("conflicting region keys for regional hostname %q: %q and %q" , change .RegionalHostname .Hostname , regionalHostname .RegionKey , change .RegionalHostname .RegionKey )
600
+ }
601
+ if (change .Action == cloudFlareUpdate && regionalHostname .Action != cloudFlareUpdate ) ||
602
+ regionalHostname .Action == cloudFlareDelete {
603
+ regionalHostnameChanges [change .RegionalHostname .Hostname ] = DataLocalizationRegionalHostnameChange {
604
+ Action : cloudFlareUpdate ,
605
+ RegionalHostname : change .RegionalHostname ,
606
+ }
607
+ }
608
+ case cloudFlareDelete :
609
+ if ! ok {
610
+ regionalHostnameChanges [change .RegionalHostname .Hostname ] = DataLocalizationRegionalHostnameChange {
611
+ Action : cloudFlareDelete ,
612
+ RegionalHostname : change .RegionalHostname ,
613
+ }
614
+ continue
615
+ }
616
+ }
617
+ }
618
+ return slices .Collect (maps .Values (regionalHostnameChanges )), nil
619
+ }
620
+
469
621
// submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
470
622
func (p * CloudFlareProvider ) submitChanges (ctx context.Context , changes []* cloudFlareChange ) error {
471
623
// return early if there is nothing to change
@@ -484,6 +636,8 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
484
636
var failedZones []string
485
637
for zoneID , zoneChanges := range changesByZone {
486
638
var failedChange bool
639
+ resourceContainer := cloudflare .ZoneIdentifier (zoneID )
640
+
487
641
for _ , change := range zoneChanges {
488
642
logFields := log.Fields {
489
643
"record" : change .ResourceRecord .Name ,
@@ -499,7 +653,6 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
499
653
continue
500
654
}
501
655
502
- resourceContainer := cloudflare .ZoneIdentifier (zoneID )
503
656
records , err := p .listDNSRecordsWithAutoPagination (ctx , zoneID )
504
657
if err != nil {
505
658
return fmt .Errorf ("could not fetch records from zone, %w" , err )
@@ -524,13 +677,6 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
524
677
failedChange = true
525
678
log .WithFields (logFields ).Errorf ("failed to update record: %v" , err )
526
679
}
527
- if regionalHostnameParam := updateDataLocalizationRegionalHostnameParams (* change ); regionalHostnameParam .RegionKey != "" {
528
- regionalHostnameErr := p .Client .UpdateDataLocalizationRegionalHostname (ctx , resourceContainer , regionalHostnameParam )
529
- if regionalHostnameErr != nil {
530
- failedChange = true
531
- log .WithFields (logFields ).Errorf ("failed to update record when editing region: %v" , regionalHostnameErr )
532
- }
533
- }
534
680
} else if change .Action == cloudFlareDelete {
535
681
recordID := p .getRecordID (records , change .ResourceRecord )
536
682
if recordID == "" {
@@ -557,6 +703,19 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
557
703
}
558
704
}
559
705
}
706
+
707
+ if regionalHostnamesChanges , err := dataLocalizationRegionalHostnamesChanges (zoneChanges ); err == nil {
708
+ if ! p .submitDataLocalizationRegionalHostnameChanges (ctx , regionalHostnamesChanges , resourceContainer ) {
709
+ failedChange = true
710
+ }
711
+ } else {
712
+ logFields := log.Fields {
713
+ "zone" : zoneID ,
714
+ }
715
+ log .WithFields (logFields ).Errorf ("failed to build data localization regional hostname changes: %v" , err )
716
+ failedChange = true
717
+ }
718
+
560
719
if failedChange {
561
720
failedZones = append (failedZones , zoneID )
562
721
}
@@ -649,7 +808,6 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
649
808
if ep .RecordTTL .IsConfigured () {
650
809
ttl = int (ep .RecordTTL )
651
810
}
652
- dt := time .Now ()
653
811
654
812
prevCustomHostnames := []string {}
655
813
newCustomHostnames := map [string ]cloudflare.CustomHostname {}
@@ -661,6 +819,13 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
661
819
newCustomHostnames [v ] = p .newCustomHostname (v , ep .DNSName )
662
820
}
663
821
}
822
+ regionalHostname := cloudflare.RegionalHostname {}
823
+ if regionKey := getRegionKey (ep , p .RegionKey ); regionKey != "" {
824
+ regionalHostname = cloudflare.RegionalHostname {
825
+ Hostname : ep .DNSName ,
826
+ RegionKey : regionKey ,
827
+ }
828
+ }
664
829
return & cloudFlareChange {
665
830
Action : action ,
666
831
ResourceRecord : cloudflare.DNSRecord {
@@ -671,15 +836,8 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
671
836
Proxied : & proxied ,
672
837
Type : ep .RecordType ,
673
838
Content : target ,
674
- Meta : map [string ]interface {}{
675
- "region" : p .RegionKey ,
676
- },
677
- },
678
- RegionalHostname : cloudflare.RegionalHostname {
679
- Hostname : ep .DNSName ,
680
- RegionKey : p .RegionKey ,
681
- CreatedOn : & dt ,
682
839
},
840
+ RegionalHostname : regionalHostname ,
683
841
CustomHostnamesPrev : prevCustomHostnames ,
684
842
CustomHostnames : newCustomHostnames ,
685
843
}
@@ -787,6 +945,19 @@ func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool {
787
945
return proxied
788
946
}
789
947
948
+ func getRegionKey (endpoint * endpoint.Endpoint , defaultRegionKey string ) string {
949
+ if ! recordTypeRegionalHostnameSupported [endpoint .RecordType ] {
950
+ return ""
951
+ }
952
+
953
+ for _ , v := range endpoint .ProviderSpecific {
954
+ if v .Name == source .CloudflareRegionKey {
955
+ return v .Value
956
+ }
957
+ }
958
+ return defaultRegionKey
959
+ }
960
+
790
961
func getEndpointCustomHostnames (ep * endpoint.Endpoint ) []string {
791
962
for _ , v := range ep .ProviderSpecific {
792
963
if v .Name == source .CloudflareCustomHostnameKey {
0 commit comments