Skip to content

Commit 2de3b50

Browse files
authored
Merge pull request #5175 from vflaux/fix_cf_regional_hostname
fix(cloudflare): regional hostnames
2 parents 0d97521 + 6f65181 commit 2de3b50

File tree

4 files changed

+704
-47
lines changed

4 files changed

+704
-47
lines changed

provider/cloudflare/cloudflare.go

Lines changed: 192 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

7574
type CustomHostnamesMap map[CustomHostnameIndex]cloudflare.CustomHostname
7675

76+
type DataLocalizationRegionalHostnameChange struct {
77+
Action string
78+
cloudflare.RegionalHostname
79+
}
80+
7781
var 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.
98108
type 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+
143160
func (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+
148169
func (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.
470622
func (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+
790961
func getEndpointCustomHostnames(ep *endpoint.Endpoint) []string {
791962
for _, v := range ep.ProviderSpecific {
792963
if v.Name == source.CloudflareCustomHostnameKey {

0 commit comments

Comments
 (0)