@@ -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,126 @@ 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+ func dataLocalizationRegionalHostnamesChanges (changes []* cloudFlareChange ) ([]DataLocalizationRegionalHostnameChange , error ) {
579+ regionalHostnameChanges := make (map [string ]DataLocalizationRegionalHostnameChange )
580+ for _ , change := range changes {
581+ if change .RegionalHostname .Hostname == "" {
582+ continue
583+ }
584+ if change .RegionalHostname .RegionKey == "" {
585+ return nil , fmt .Errorf ("region key is empty for regional hostname %q" , change .RegionalHostname .Hostname )
586+ }
587+ regionalHostname , ok := regionalHostnameChanges [change .RegionalHostname .Hostname ]
588+ switch change .Action {
589+ case cloudFlareCreate , cloudFlareUpdate :
590+ if ! ok {
591+ regionalHostnameChanges [change .RegionalHostname .Hostname ] = DataLocalizationRegionalHostnameChange {
592+ Action : change .Action ,
593+ RegionalHostname : change .RegionalHostname ,
594+ }
595+ continue
596+ }
597+ if regionalHostname .RegionKey != change .RegionalHostname .RegionKey {
598+ return nil , fmt .Errorf ("conflicting region keys for regional hostname %q: %q and %q" , change .RegionalHostname .Hostname , regionalHostname .RegionKey , change .RegionalHostname .RegionKey )
599+ }
600+ if change .Action == cloudFlareUpdate {
601+ regionalHostname .Action = cloudFlareUpdate
602+ } else if regionalHostname .Action == cloudFlareDelete {
603+ regionalHostname .Action = cloudFlareUpdate
604+ }
605+ case cloudFlareDelete :
606+ if ! ok {
607+ regionalHostnameChanges [change .RegionalHostname .Hostname ] = DataLocalizationRegionalHostnameChange {
608+ Action : cloudFlareDelete ,
609+ RegionalHostname : change .RegionalHostname ,
610+ }
611+ continue
612+ }
613+ }
614+ }
615+ return slices .Collect (maps .Values (regionalHostnameChanges )), nil
616+ }
617+
469618// submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
470619func (p * CloudFlareProvider ) submitChanges (ctx context.Context , changes []* cloudFlareChange ) error {
471620 // return early if there is nothing to change
@@ -484,6 +633,8 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
484633 var failedZones []string
485634 for zoneID , zoneChanges := range changesByZone {
486635 var failedChange bool
636+ resourceContainer := cloudflare .ZoneIdentifier (zoneID )
637+
487638 for _ , change := range zoneChanges {
488639 logFields := log.Fields {
489640 "record" : change .ResourceRecord .Name ,
@@ -499,7 +650,6 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
499650 continue
500651 }
501652
502- resourceContainer := cloudflare .ZoneIdentifier (zoneID )
503653 records , err := p .listDNSRecordsWithAutoPagination (ctx , zoneID )
504654 if err != nil {
505655 return fmt .Errorf ("could not fetch records from zone, %w" , err )
@@ -524,13 +674,6 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
524674 failedChange = true
525675 log .WithFields (logFields ).Errorf ("failed to update record: %v" , err )
526676 }
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- }
534677 } else if change .Action == cloudFlareDelete {
535678 recordID := p .getRecordID (records , change .ResourceRecord )
536679 if recordID == "" {
@@ -557,6 +700,19 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
557700 }
558701 }
559702 }
703+
704+ if regionalHostnamesChanges , err := dataLocalizationRegionalHostnamesChanges (zoneChanges ); err == nil {
705+ if ! p .submitDataLocalizationRegionalHostnameChanges (ctx , regionalHostnamesChanges , resourceContainer ) {
706+ failedChange = true
707+ }
708+ } else {
709+ logFields := log.Fields {
710+ "zone" : zoneID ,
711+ }
712+ log .WithFields (logFields ).Errorf ("failed to build data localization regional hostname changes: %v" , err )
713+ failedChange = true
714+ }
715+
560716 if failedChange {
561717 failedZones = append (failedZones , zoneID )
562718 }
@@ -649,7 +805,6 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
649805 if ep .RecordTTL .IsConfigured () {
650806 ttl = int (ep .RecordTTL )
651807 }
652- dt := time .Now ()
653808
654809 prevCustomHostnames := []string {}
655810 newCustomHostnames := map [string ]cloudflare.CustomHostname {}
@@ -661,6 +816,13 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
661816 newCustomHostnames [v ] = p .newCustomHostname (v , ep .DNSName )
662817 }
663818 }
819+ regionalHostname := cloudflare.RegionalHostname {}
820+ if regionKey := getRegionKey (ep , p .RegionKey ); regionKey != "" {
821+ regionalHostname = cloudflare.RegionalHostname {
822+ Hostname : ep .DNSName ,
823+ RegionKey : regionKey ,
824+ }
825+ }
664826 return & cloudFlareChange {
665827 Action : action ,
666828 ResourceRecord : cloudflare.DNSRecord {
@@ -671,15 +833,8 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
671833 Proxied : & proxied ,
672834 Type : ep .RecordType ,
673835 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 ,
682836 },
837+ RegionalHostname : regionalHostname ,
683838 CustomHostnamesPrev : prevCustomHostnames ,
684839 CustomHostnames : newCustomHostnames ,
685840 }
@@ -787,6 +942,19 @@ func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool {
787942 return proxied
788943}
789944
945+ func getRegionKey (endpoint * endpoint.Endpoint , defaultRegionKey string ) string {
946+ if ! recordTypeRegionalHostnameSupported [endpoint .RecordType ] {
947+ return ""
948+ }
949+
950+ for _ , v := range endpoint .ProviderSpecific {
951+ if v .Name == source .CloudflareRegionKey {
952+ return v .Value
953+ }
954+ }
955+ return defaultRegionKey
956+ }
957+
790958func getEndpointCustomHostnames (ep * endpoint.Endpoint ) []string {
791959 for _ , v := range ep .ProviderSpecific {
792960 if v .Name == source .CloudflareCustomHostnameKey {
0 commit comments