@@ -27,7 +27,6 @@ import (
2727 "sort"
2828 "strconv"
2929 "strings"
30- "time"
3130
3231 cloudflare "github.com/cloudflare/cloudflare-go"
3332 log "github.com/sirupsen/logrus"
@@ -74,6 +73,11 @@ type CustomHostnameIndex struct {
7473
7574type CustomHostnamesMap map [CustomHostnameIndex ]cloudflare.CustomHostname
7675
76+ type DataLocalizationRegionalHostnameChange struct {
77+ Action string
78+ cloudflare.RegionalHostname
79+ }
80+
7781var recordTypeProxyNotSupported = map [string ]bool {
7882 "LOC" : true ,
7983 "MX" : true ,
@@ -94,6 +98,12 @@ var recordTypeCustomHostnameSupported = map[string]bool{
9498 "CNAME" : true ,
9599}
96100
101+ var recordTypeRegionalHostnameSupported = map [string ]bool {
102+ "A" : true ,
103+ "AAAA" : true ,
104+ "CNAME" : true ,
105+ }
106+
97107// cloudFlareDNS is the subset of the CloudFlare API that we actually use. Add methods as required. Signatures must match exactly.
98108type cloudFlareDNS interface {
99109 UserDetails (ctx context.Context ) (cloudflare.User , error )
@@ -105,7 +115,9 @@ type cloudFlareDNS interface {
105115 CreateDNSRecord (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.CreateDNSRecordParams ) (cloudflare.DNSRecord , error )
106116 DeleteDNSRecord (ctx context.Context , rc * cloudflare.ResourceContainer , recordID string ) error
107117 UpdateDNSRecord (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.UpdateDNSRecordParams ) error
118+ CreateDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.CreateDataLocalizationRegionalHostnameParams ) error
108119 UpdateDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.UpdateDataLocalizationRegionalHostnameParams ) error
120+ DeleteDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , hostname string ) error
109121 CustomHostnames (ctx context.Context , zoneID string , page int , filter cloudflare.CustomHostname ) ([]cloudflare.CustomHostname , cloudflare.ResultInfo , error )
110122 DeleteCustomHostname (ctx context.Context , zoneID string , customHostnameID string ) error
111123 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
140152 return err
141153}
142154
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+
143160func (z zoneService ) UpdateDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.UpdateDataLocalizationRegionalHostnameParams ) error {
144161 _ , err := z .service .UpdateDataLocalizationRegionalHostname (ctx , rc , rp )
145162 return err
146163}
147164
165+ func (z zoneService ) DeleteDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , hostname string ) error {
166+ return z .service .DeleteDataLocalizationRegionalHostname (ctx , rc , hostname )
167+ }
168+
148169func (z zoneService ) DeleteDNSRecord (ctx context.Context , rc * cloudflare.ResourceContainer , recordID string ) error {
149170 return z .service .DeleteDNSRecord (ctx , rc , recordID )
150171}
@@ -208,11 +229,19 @@ func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams
208229 }
209230}
210231
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+
211240// 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 {
213242 return cloudflare.UpdateDataLocalizationRegionalHostnameParams {
214- Hostname : cfc . RegionalHostname .Hostname ,
215- RegionKey : cfc . RegionalHostname .RegionKey ,
243+ Hostname : rhc .Hostname ,
244+ RegionKey : rhc .RegionKey ,
216245 }
217246}
218247
@@ -466,6 +495,129 @@ func (p *CloudFlareProvider) submitCustomHostnameChanges(ctx context.Context, zo
466495 return ! failedChange
467496}
468497
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+
469621// submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
470622func (p * CloudFlareProvider ) submitChanges (ctx context.Context , changes []* cloudFlareChange ) error {
471623 // return early if there is nothing to change
@@ -484,6 +636,8 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
484636 var failedZones []string
485637 for zoneID , zoneChanges := range changesByZone {
486638 var failedChange bool
639+ resourceContainer := cloudflare .ZoneIdentifier (zoneID )
640+
487641 for _ , change := range zoneChanges {
488642 logFields := log.Fields {
489643 "record" : change .ResourceRecord .Name ,
@@ -499,7 +653,6 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
499653 continue
500654 }
501655
502- resourceContainer := cloudflare .ZoneIdentifier (zoneID )
503656 records , err := p .listDNSRecordsWithAutoPagination (ctx , zoneID )
504657 if err != nil {
505658 return fmt .Errorf ("could not fetch records from zone, %w" , err )
@@ -524,13 +677,6 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
524677 failedChange = true
525678 log .WithFields (logFields ).Errorf ("failed to update record: %v" , err )
526679 }
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- }
534680 } else if change .Action == cloudFlareDelete {
535681 recordID := p .getRecordID (records , change .ResourceRecord )
536682 if recordID == "" {
@@ -557,6 +703,19 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
557703 }
558704 }
559705 }
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+
560719 if failedChange {
561720 failedZones = append (failedZones , zoneID )
562721 }
@@ -649,7 +808,6 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
649808 if ep .RecordTTL .IsConfigured () {
650809 ttl = int (ep .RecordTTL )
651810 }
652- dt := time .Now ()
653811
654812 prevCustomHostnames := []string {}
655813 newCustomHostnames := map [string ]cloudflare.CustomHostname {}
@@ -661,6 +819,13 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
661819 newCustomHostnames [v ] = p .newCustomHostname (v , ep .DNSName )
662820 }
663821 }
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+ }
664829 return & cloudFlareChange {
665830 Action : action ,
666831 ResourceRecord : cloudflare.DNSRecord {
@@ -671,15 +836,8 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
671836 Proxied : & proxied ,
672837 Type : ep .RecordType ,
673838 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 ,
682839 },
840+ RegionalHostname : regionalHostname ,
683841 CustomHostnamesPrev : prevCustomHostnames ,
684842 CustomHostnames : newCustomHostnames ,
685843 }
@@ -787,6 +945,19 @@ func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool {
787945 return proxied
788946}
789947
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+
790961func getEndpointCustomHostnames (ep * endpoint.Endpoint ) []string {
791962 for _ , v := range ep .ProviderSpecific {
792963 if v .Name == source .CloudflareCustomHostnameKey {
0 commit comments