@@ -64,6 +64,11 @@ var recordTypeProxyNotSupported = map[string]bool{
6464 "SRV" : true ,
6565}
6666
67+ var recordTypeCustomHostnameSupported = map [string ]bool {
68+ "A" : true ,
69+ "CNAME" : true ,
70+ }
71+
6772// cloudFlareDNS is the subset of the CloudFlare API that we actually use. Add methods as required. Signatures must match exactly.
6873type cloudFlareDNS interface {
6974 UserDetails (ctx context.Context ) (cloudflare.User , error )
@@ -76,6 +81,9 @@ type cloudFlareDNS interface {
7681 DeleteDNSRecord (ctx context.Context , rc * cloudflare.ResourceContainer , recordID string ) error
7782 UpdateDNSRecord (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.UpdateDNSRecordParams ) error
7883 UpdateDataLocalizationRegionalHostname (ctx context.Context , rc * cloudflare.ResourceContainer , rp cloudflare.UpdateDataLocalizationRegionalHostnameParams ) error
84+ CustomHostnames (ctx context.Context , zoneID string , page int , filter cloudflare.CustomHostname ) ([]cloudflare.CustomHostname , cloudflare.ResultInfo , error )
85+ DeleteCustomHostname (ctx context.Context , zoneID string , customHostnameID string ) error
86+ CreateCustomHostname (ctx context.Context , zoneID string , ch cloudflare.CustomHostname ) (* cloudflare.CustomHostnameResponse , error )
7987}
8088
8189type zoneService struct {
@@ -124,6 +132,18 @@ func (z zoneService) ZoneDetails(ctx context.Context, zoneID string) (cloudflare
124132 return z .service .ZoneDetails (ctx , zoneID )
125133}
126134
135+ func (z zoneService ) CustomHostnames (ctx context.Context , zoneID string , page int , filter cloudflare.CustomHostname ) ([]cloudflare.CustomHostname , cloudflare.ResultInfo , error ) {
136+ return z .service .CustomHostnames (ctx , zoneID , page , filter )
137+ }
138+
139+ func (z zoneService ) DeleteCustomHostname (ctx context.Context , zoneID string , customHostnameID string ) error {
140+ return z .service .DeleteCustomHostname (ctx , zoneID , customHostnameID )
141+ }
142+
143+ func (z zoneService ) CreateCustomHostname (ctx context.Context , zoneID string , ch cloudflare.CustomHostname ) (* cloudflare.CustomHostnameResponse , error ) {
144+ return z .service .CreateCustomHostname (ctx , zoneID , ch )
145+ }
146+
127147// CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
128148type CloudFlareProvider struct {
129149 provider.BaseProvider
@@ -142,6 +162,7 @@ type cloudFlareChange struct {
142162 Action string
143163 ResourceRecord cloudflare.DNSRecord
144164 RegionalHostname cloudflare.RegionalHostname
165+ CustomHostname cloudflare.CustomHostname
145166}
146167
147168// RecordParamsTypes is a typeset of the possible Record Params that can be passed to cloudflare-go library
@@ -278,10 +299,15 @@ func (p *CloudFlareProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
278299 return nil , err
279300 }
280301
302+ chs , chErr := p .listCustomHostnamesWithPagination (ctx , zone .ID )
303+ if chErr != nil {
304+ return nil , chErr
305+ }
306+
281307 // As CloudFlare does not support "sets" of targets, but instead returns
282308 // a single entry for each name/type/target, we have to group by name
283309 // and record to allow the planner to calculate the correct plan. See #992.
284- endpoints = append (endpoints , groupByNameAndType (records )... )
310+ endpoints = append (endpoints , groupByNameAndTypeWithCustomHostnames (records , chs )... )
285311 }
286312
287313 return endpoints , nil
@@ -346,6 +372,11 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
346372 return fmt .Errorf ("could not fetch records from zone, %v" , err )
347373 }
348374
375+ chs , chErr := p .listCustomHostnamesWithPagination (ctx , zoneID )
376+ if chErr != nil {
377+ return fmt .Errorf ("could not fetch custom hostnames from zone, %v" , chErr )
378+ }
379+
349380 var failedChange bool
350381 for _ , change := range changes {
351382 logFields := log.Fields {
@@ -364,23 +395,54 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
364395
365396 resourceContainer := cloudflare .ZoneIdentifier (zoneID )
366397 if change .Action == cloudFlareUpdate {
398+ if recordTypeCustomHostnameSupported [change .ResourceRecord .Type ] {
399+ chID , oldCh := p .getCustomHostnameIDbyOrigin (chs , change .ResourceRecord .Name )
400+ if chID == "" && change .CustomHostname .Hostname != "" {
401+ log .WithFields (logFields ).Infof ("Adding custom hostname %v" , change .CustomHostname .Hostname )
402+ _ , chErr := p .Client .CreateCustomHostname (ctx , zoneID , change .CustomHostname )
403+ if chErr != nil {
404+ failedChange = true
405+ log .WithFields (logFields ).Errorf ("failed to add custom hostname %v: %v" , change .CustomHostname .Hostname , chErr )
406+ }
407+ } else if chID != "" && oldCh != "" && change .CustomHostname .Hostname == "" {
408+ log .WithFields (logFields ).Infof ("Removing custom hostname %v" , change .CustomHostname .Hostname )
409+ chErr := p .Client .DeleteCustomHostname (ctx , zoneID , chID )
410+ if chErr != nil {
411+ failedChange = true
412+ log .WithFields (logFields ).Errorf ("failed to remove custom hostname %v: %v" , change .CustomHostname .Hostname , chErr )
413+ }
414+ } else if chID != "" && change .CustomHostname .Hostname != "" && oldCh != change .CustomHostname .Hostname {
415+ log .WithFields (logFields ).Infof ("Replacing custom hostname: %v/%v to %v" , chID , oldCh , change .CustomHostname .Hostname )
416+ chDelErr := p .Client .DeleteCustomHostname (ctx , zoneID , chID )
417+ if chDelErr != nil {
418+ failedChange = true
419+ log .WithFields (logFields ).Errorf ("failed to remove replacing custom hostname %v/%v: %v" , chID , oldCh , chDelErr )
420+ }
421+ _ , chAddErr := p .Client .CreateCustomHostname (ctx , zoneID , change .CustomHostname )
422+ if chAddErr != nil {
423+ failedChange = true
424+ log .WithFields (logFields ).Errorf ("failed to add replacing custom hostname %v: %v" , change .CustomHostname .Hostname , chAddErr )
425+ }
426+ }
427+ }
367428 recordID := p .getRecordID (records , change .ResourceRecord )
368429 if recordID == "" {
369430 log .WithFields (logFields ).Errorf ("failed to find previous record: %v" , change .ResourceRecord )
370431 continue
371432 }
372433 recordParam := updateDNSRecordParam (* change )
373- regionalHostnameParam := updateDataLocalizationRegionalHostnameParams (* change )
374434 recordParam .ID = recordID
375435 err := p .Client .UpdateDNSRecord (ctx , resourceContainer , recordParam )
376436 if err != nil {
377437 failedChange = true
378438 log .WithFields (logFields ).Errorf ("failed to update record: %v" , err )
379439 }
380- regionalHostnameErr := p .Client .UpdateDataLocalizationRegionalHostname (ctx , resourceContainer , regionalHostnameParam )
381- if regionalHostnameErr != nil {
382- failedChange = true
383- log .WithFields (logFields ).Errorf ("failed to update record when editing region: %v" , regionalHostnameErr )
440+ if regionalHostnameParam := updateDataLocalizationRegionalHostnameParams (* change ); regionalHostnameParam .RegionKey != "" {
441+ regionalHostnameErr := p .Client .UpdateDataLocalizationRegionalHostname (ctx , resourceContainer , regionalHostnameParam )
442+ if regionalHostnameErr != nil {
443+ failedChange = true
444+ log .WithFields (logFields ).Errorf ("failed to update record when editing region: %v" , regionalHostnameErr )
445+ }
384446 }
385447 } else if change .Action == cloudFlareDelete {
386448 recordID := p .getRecordID (records , change .ResourceRecord )
@@ -393,13 +455,28 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
393455 failedChange = true
394456 log .WithFields (logFields ).Errorf ("failed to delete record: %v" , err )
395457 }
458+ chID , oldCh := p .getCustomHostnameIDbyOrigin (chs , change .ResourceRecord .Name )
459+ if chID == "" {
460+ continue
461+ }
462+ chErr := p .Client .DeleteCustomHostname (ctx , zoneID , chID )
463+ if chErr != nil {
464+ failedChange = true
465+ log .WithFields (logFields ).Errorf ("failed to delete custom hostname %v/%v: %v" , chID , oldCh , chErr )
466+ }
396467 } else if change .Action == cloudFlareCreate {
397468 recordParam := getCreateDNSRecordParam (* change )
398469 _ , err := p .Client .CreateDNSRecord (ctx , resourceContainer , recordParam )
399470 if err != nil {
400471 failedChange = true
401472 log .WithFields (logFields ).Errorf ("failed to create record: %v" , err )
402473 }
474+ log .WithFields (logFields ).Infof ("Creating custom hostname %v" , change .CustomHostname .Hostname )
475+ _ , chErr := p .Client .CreateCustomHostname (ctx , zoneID , change .CustomHostname )
476+ if chErr != nil {
477+ failedChange = true
478+ log .WithFields (logFields ).Errorf ("failed to create custom hostname %v: %v" , change .CustomHostname .Hostname , chErr )
479+ }
403480 }
404481 }
405482
@@ -461,6 +538,15 @@ func (p *CloudFlareProvider) getRecordID(records []cloudflare.DNSRecord, record
461538 return ""
462539}
463540
541+ func (p * CloudFlareProvider ) getCustomHostnameIDbyOrigin (chs []cloudflare.CustomHostname , origin string ) (string , string ) {
542+ for _ , zoneCh := range chs {
543+ if zoneCh .CustomOriginServer == origin {
544+ return zoneCh .ID , zoneCh .Hostname
545+ }
546+ }
547+ return "" , ""
548+ }
549+
464550func (p * CloudFlareProvider ) newCloudFlareChange (action string , endpoint * endpoint.Endpoint , target string ) * cloudFlareChange {
465551 ttl := defaultCloudFlareRecordTTL
466552 proxied := shouldBeProxied (endpoint , p .proxiedByDefault )
@@ -472,8 +558,10 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi
472558 return & cloudFlareChange {
473559 Action : action ,
474560 ResourceRecord : cloudflare.DNSRecord {
475- Name : endpoint .DNSName ,
476- TTL : ttl ,
561+ Name : endpoint .DNSName ,
562+ TTL : ttl ,
563+ // We have to use pointers to bools now, as the upstream cloudflare-go library requires them
564+ // see: https://github.com/cloudflare/cloudflare-go/pull/595
477565 Proxied : & proxied ,
478566 Type : endpoint .RecordType ,
479567 Content : target ,
@@ -486,10 +574,23 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi
486574 RegionKey : p .RegionKey ,
487575 CreatedOn : & dt ,
488576 },
577+ CustomHostname : cloudflare.CustomHostname {
578+ Hostname : getEndpointCustomHostname (endpoint ),
579+ CustomOriginServer : endpoint .DNSName ,
580+ SSL : & cloudflare.CustomHostnameSSL {
581+ Type : "dv" ,
582+ Method : "http" ,
583+ CertificateAuthority : "google" ,
584+ BundleMethod : "ubiquitous" ,
585+ Settings : cloudflare.CustomHostnameSSLSettings {
586+ MinTLSVersion : "1.0" ,
587+ },
588+ },
589+ },
489590 }
490591}
491592
492- // listDNSRecords performs automatic pagination of results on requests to cloudflare.ListDNSRecords with custom per_page values
593+ // listDNSRecordsWithAutoPagination performs automatic pagination of results on requests to cloudflare.ListDNSRecords with custom per_page values
493594func (p * CloudFlareProvider ) listDNSRecordsWithAutoPagination (ctx context.Context , zoneID string ) ([]cloudflare.DNSRecord , error ) {
494595 var records []cloudflare.DNSRecord
495596 resultInfo := cloudflare.ResultInfo {PerPage : p .DNSRecordsPerPage , Page : 1 }
@@ -516,6 +617,33 @@ func (p *CloudFlareProvider) listDNSRecordsWithAutoPagination(ctx context.Contex
516617 return records , nil
517618}
518619
620+ // listCustomHostnamesWithPagination performs automatic pagination of results on requests to cloudflare.CustomHostnames
621+ func (p * CloudFlareProvider ) listCustomHostnamesWithPagination (ctx context.Context , zoneID string ) ([]cloudflare.CustomHostname , error ) {
622+ var chs []cloudflare.CustomHostname
623+ resultInfo := cloudflare.ResultInfo {Page : 1 }
624+ for {
625+ pageCustomHostnameListResponse , resultInfo , err := p .Client .CustomHostnames (ctx , zoneID , resultInfo .Page , cloudflare.CustomHostname {})
626+ if err != nil {
627+ var apiErr * cloudflare.Error
628+ if errors .As (err , & apiErr ) {
629+ if apiErr .ClientRateLimited () || apiErr .StatusCode >= http .StatusInternalServerError {
630+ // Handle rate limit error as a soft error
631+ return nil , provider .NewSoftError (err )
632+ }
633+ }
634+ log .Errorf ("zone %s failed to fetch custom hostnames. Please check if \" Cloudflare for SaaS\" is enabled and API key permissions, %v" , zoneID , err )
635+ return nil , err
636+ }
637+
638+ chs = append (chs , pageCustomHostnameListResponse ... )
639+ resultInfo = resultInfo .Next ()
640+ if resultInfo .Done () {
641+ break
642+ }
643+ }
644+ return chs , nil
645+ }
646+
519647func shouldBeProxied (endpoint * endpoint.Endpoint , proxiedByDefault bool ) bool {
520648 proxied := proxiedByDefault
521649
@@ -537,7 +665,16 @@ func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool {
537665 return proxied
538666}
539667
540- func groupByNameAndType (records []cloudflare.DNSRecord ) []* endpoint.Endpoint {
668+ func getEndpointCustomHostname (endpoint * endpoint.Endpoint ) string {
669+ for _ , v := range endpoint .ProviderSpecific {
670+ if v .Name == source .CloudflareCustomHostnameKey {
671+ return v .Value
672+ }
673+ }
674+ return ""
675+ }
676+
677+ func groupByNameAndTypeWithCustomHostnames (records []cloudflare.DNSRecord , chs []cloudflare.CustomHostname ) []* endpoint.Endpoint {
541678 endpoints := []* endpoint.Endpoint {}
542679
543680 // group supported records by name and type
@@ -556,20 +693,41 @@ func groupByNameAndType(records []cloudflare.DNSRecord) []*endpoint.Endpoint {
556693 groups [groupBy ] = append (groups [groupBy ], r )
557694 }
558695
696+ // map custom origin to custom hostname, custom origin should match to a dns record
697+ customOriginServers := map [string ]string {}
698+
699+ // only one latest custom hostname for a dns record would work
700+ for _ , c := range chs {
701+ customOriginServers [c .CustomOriginServer ] = c .Hostname
702+ }
703+
559704 // create single endpoint with all the targets for each name/type
560705 for _ , records := range groups {
706+ if len (records ) == 0 {
707+ return endpoints
708+ }
561709 targets := make ([]string , len (records ))
562710 for i , record := range records {
563711 targets [i ] = record .Content
564712 }
565- endpoints = append (endpoints ,
566- endpoint .NewEndpointWithTTL (
567- records [0 ].Name ,
568- records [0 ].Type ,
569- endpoint .TTL (records [0 ].TTL ),
570- targets ... ).
571- WithProviderSpecific (source .CloudflareProxiedKey , strconv .FormatBool (* records [0 ].Proxied )),
572- )
713+ ep := endpoint .NewEndpointWithTTL (
714+ records [0 ].Name ,
715+ records [0 ].Type ,
716+ endpoint .TTL (records [0 ].TTL ),
717+ targets ... )
718+ proxied := false
719+ if records [0 ].Proxied != nil {
720+ proxied = * records [0 ].Proxied
721+ }
722+ if ep == nil {
723+ continue
724+ }
725+ ep .WithProviderSpecific (source .CloudflareProxiedKey , strconv .FormatBool (proxied ))
726+ if customHostname , ok := customOriginServers [records [0 ].Name ]; ok {
727+ ep .WithProviderSpecific (source .CloudflareCustomHostnameKey , customHostname )
728+ }
729+
730+ endpoints = append (endpoints , ep )
573731 }
574732
575733 return endpoints
0 commit comments