chore(cloudflare): use lib v4 for regional services (#5609)

* chore(cloudflare): add cloudflare v4 client

* chrome(cloudflare): cli v4 for regional hostanmes
This commit is contained in:
vflaux 2025-07-11 18:47:36 +02:00 committed by GitHub
parent 385327e2e1
commit e2b56049f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 246 additions and 62 deletions

5
go.mod
View File

@ -25,6 +25,7 @@ require (
github.com/cenkalti/backoff/v5 v5.0.2 github.com/cenkalti/backoff/v5 v5.0.2
github.com/civo/civogo v0.6.1 github.com/civo/civogo v0.6.1
github.com/cloudflare/cloudflare-go v0.115.0 github.com/cloudflare/cloudflare-go v0.115.0
github.com/cloudflare/cloudflare-go/v4 v4.5.1
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
github.com/datawire/ambassador v1.12.4 github.com/datawire/ambassador v1.12.4
github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace
@ -162,6 +163,10 @@ require (
github.com/sosodev/duration v1.3.1 // indirect github.com/sosodev/duration v1.3.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/vektah/gqlparser/v2 v2.5.26 // indirect github.com/vektah/gqlparser/v2 v2.5.26 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect

12
go.sum
View File

@ -186,6 +186,8 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM= github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM=
github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU=
github.com/cloudflare/cloudflare-go/v4 v4.5.1 h1:ZQgQ7QO+M9rK0KYx1CmppuG15ZTYGHn8F9/Fh7mCuQQ=
github.com/cloudflare/cloudflare-go/v4 v4.5.1/go.mod h1:XcYpLe7Mf6FN87kXzEWVnJ6z+vskW/k6eUqgqfhFE9k=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s= github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14= github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
@ -990,7 +992,17 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550= github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550=

View File

@ -29,6 +29,9 @@ import (
"strings" "strings"
"github.com/cloudflare/cloudflare-go" "github.com/cloudflare/cloudflare-go"
cloudflarev4 "github.com/cloudflare/cloudflare-go/v4"
"github.com/cloudflare/cloudflare-go/v4/addressing"
"github.com/cloudflare/cloudflare-go/v4/option"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
@ -109,17 +112,18 @@ type cloudFlareDNS interface {
CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error)
DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error
UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error
ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error) ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse]
CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error
UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error
DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error
CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error) CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error)
DeleteCustomHostname(ctx context.Context, zoneID string, customHostnameID string) error DeleteCustomHostname(ctx context.Context, zoneID string, customHostnameID string) error
CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error) CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error)
} }
type zoneService struct { type zoneService struct {
service *cloudflare.API service *cloudflare.API
serviceV4 *cloudflarev4.Client
} }
func (z zoneService) ZoneIDByName(zoneName string) (string, error) { func (z zoneService) ZoneIDByName(zoneName string) (string, error) {
@ -287,8 +291,9 @@ func NewCloudFlareProvider(
) (*CloudFlareProvider, error) { ) (*CloudFlareProvider, error) {
// initialize via chosen auth method and returns new API object // initialize via chosen auth method and returns new API object
var ( var (
config *cloudflare.API config *cloudflare.API
err error configV4 *cloudflarev4.Client
err error
) )
if os.Getenv("CF_API_TOKEN") != "" { if os.Getenv("CF_API_TOKEN") != "" {
token := os.Getenv("CF_API_TOKEN") token := os.Getenv("CF_API_TOKEN")
@ -300,8 +305,15 @@ func NewCloudFlareProvider(
token = strings.TrimSpace(string(tokenBytes)) token = strings.TrimSpace(string(tokenBytes))
} }
config, err = cloudflare.NewWithAPIToken(token) config, err = cloudflare.NewWithAPIToken(token)
configV4 = cloudflarev4.NewClient(
option.WithAPIToken(token),
)
} else { } else {
config, err = cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL")) config, err = cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL"))
configV4 = cloudflarev4.NewClient(
option.WithAPIKey(os.Getenv("CF_API_KEY")),
option.WithAPIEmail(os.Getenv("CF_API_EMAIL")),
)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize cloudflare provider: %w", err) return nil, fmt.Errorf("failed to initialize cloudflare provider: %w", err)
@ -312,7 +324,7 @@ func NewCloudFlareProvider(
} }
return &CloudFlareProvider{ return &CloudFlareProvider{
Client: zoneService{config}, Client: zoneService{config, configV4},
domainFilter: domainFilter, domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter, zoneIDFilter: zoneIDFilter,
proxiedByDefault: proxiedByDefault, proxiedByDefault: proxiedByDefault,
@ -646,12 +658,12 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
return fmt.Errorf("failed to build desired regional hostnames: %w", err) return fmt.Errorf("failed to build desired regional hostnames: %w", err)
} }
if len(desiredRegionalHostnames) > 0 { if len(desiredRegionalHostnames) > 0 {
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, resourceContainer) regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, zoneID)
if err != nil { if err != nil {
return fmt.Errorf("could not fetch regional hostnames from zone, %w", err) return fmt.Errorf("could not fetch regional hostnames from zone, %w", err)
} }
regionalHostnamesChanges := regionalHostnamesChanges(desiredRegionalHostnames, regionalHostnames) regionalHostnamesChanges := regionalHostnamesChanges(desiredRegionalHostnames, regionalHostnames)
if !p.submitRegionalHostnameChanges(ctx, regionalHostnamesChanges, resourceContainer) { if !p.submitRegionalHostnameChanges(ctx, zoneID, regionalHostnamesChanges) {
failedChange = true failedChange = true
} }
} }

View File

@ -22,7 +22,9 @@ import (
"maps" "maps"
"slices" "slices"
cloudflare "github.com/cloudflare/cloudflare-go" cloudflarev4 "github.com/cloudflare/cloudflare-go/v4"
"github.com/cloudflare/cloudflare-go/v4/addressing"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
@ -53,46 +55,62 @@ type regionalHostnameChange struct {
regionalHostname regionalHostname
} }
func (z zoneService) ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error) { func (z zoneService) ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse] {
return z.service.ListDataLocalizationRegionalHostnames(ctx, rc, rp) return z.serviceV4.Addressing.RegionalHostnames.ListAutoPaging(ctx, params)
} }
func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error { func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error {
_, err := z.service.CreateDataLocalizationRegionalHostname(ctx, rc, rp) _, err := z.serviceV4.Addressing.RegionalHostnames.New(ctx, params)
return err return err
} }
func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error { func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error {
_, err := z.service.UpdateDataLocalizationRegionalHostname(ctx, rc, rp) _, err := z.serviceV4.Addressing.RegionalHostnames.Edit(ctx, hostname, params)
return err return err
} }
func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error { func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error {
return z.service.DeleteDataLocalizationRegionalHostname(ctx, rc, hostname) _, err := z.serviceV4.Addressing.RegionalHostnames.Delete(ctx, hostname, params)
return err
}
// listDataLocalizationRegionalHostnamesParams is a function that returns the appropriate RegionalHostname List Param based on the zoneID
func listDataLocalizationRegionalHostnamesParams(zoneID string) addressing.RegionalHostnameListParams {
return addressing.RegionalHostnameListParams{
ZoneID: cloudflarev4.F(zoneID),
}
} }
// createDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in // createDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
func createDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.CreateDataLocalizationRegionalHostnameParams { func createDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameNewParams {
return cloudflare.CreateDataLocalizationRegionalHostnameParams{ return addressing.RegionalHostnameNewParams{
Hostname: rhc.hostname, ZoneID: cloudflarev4.F(zoneID),
RegionKey: rhc.regionKey, Hostname: cloudflarev4.F(rhc.hostname),
RegionKey: cloudflarev4.F(rhc.regionKey),
} }
} }
// updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in // updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
func updateDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.UpdateDataLocalizationRegionalHostnameParams { func updateDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameEditParams {
return cloudflare.UpdateDataLocalizationRegionalHostnameParams{ return addressing.RegionalHostnameEditParams{
Hostname: rhc.hostname, ZoneID: cloudflarev4.F(zoneID),
RegionKey: rhc.regionKey, RegionKey: cloudflarev4.F(rhc.regionKey),
}
}
// deleteDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
func deleteDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameDeleteParams {
return addressing.RegionalHostnameDeleteParams{
ZoneID: cloudflarev4.F(zoneID),
} }
} }
// submitRegionalHostnameChanges applies a set of regional hostname changes, returns false if at least one fails // submitRegionalHostnameChanges applies a set of regional hostname changes, returns false if at least one fails
func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context, rhChanges []regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool { func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context, zoneID string, rhChanges []regionalHostnameChange) bool {
failedChange := false failedChange := false
for _, rhChange := range rhChanges { for _, rhChange := range rhChanges {
if !p.submitRegionalHostnameChange(ctx, rhChange, resourceContainer) { if !p.submitRegionalHostnameChange(ctx, zoneID, rhChange) {
failedChange = true failedChange = true
} }
} }
@ -101,12 +119,12 @@ func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context,
} }
// submitRegionalHostnameChange applies a single regional hostname change, returns false if it fails // submitRegionalHostnameChange applies a single regional hostname change, returns false if it fails
func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, rhChange regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool { func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, zoneID string, rhChange regionalHostnameChange) bool {
changeLog := log.WithFields(log.Fields{ changeLog := log.WithFields(log.Fields{
"hostname": rhChange.hostname, "hostname": rhChange.hostname,
"region_key": rhChange.regionKey, "region_key": rhChange.regionKey,
"action": rhChange.action.String(), "action": rhChange.action.String(),
"zone": resourceContainer.Identifier, "zone": zoneID,
}) })
if p.DryRun { if p.DryRun {
changeLog.Debug("Dry run: skipping regional hostname change", rhChange.action) changeLog.Debug("Dry run: skipping regional hostname change", rhChange.action)
@ -115,21 +133,22 @@ func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, r
switch rhChange.action { switch rhChange.action {
case cloudFlareCreate: case cloudFlareCreate:
changeLog.Debug("Creating regional hostname") changeLog.Debug("Creating regional hostname")
regionalHostnameParam := createDataLocalizationRegionalHostnameParams(rhChange) params := createDataLocalizationRegionalHostnameParams(zoneID, rhChange)
if err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam); err != nil { if err := p.Client.CreateDataLocalizationRegionalHostname(ctx, params); err != nil {
changeLog.Errorf("failed to create regional hostname: %v", err) changeLog.Errorf("failed to create regional hostname: %v", err)
return false return false
} }
case cloudFlareUpdate: case cloudFlareUpdate:
changeLog.Debug("Updating regional hostname") changeLog.Debug("Updating regional hostname")
regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(rhChange) params := updateDataLocalizationRegionalHostnameParams(zoneID, rhChange)
if err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam); err != nil { if err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, rhChange.hostname, params); err != nil {
changeLog.Errorf("failed to update regional hostname: %v", err) changeLog.Errorf("failed to update regional hostname: %v", err)
return false return false
} }
case cloudFlareDelete: case cloudFlareDelete:
changeLog.Debug("Deleting regional hostname") changeLog.Debug("Deleting regional hostname")
if err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, resourceContainer, rhChange.hostname); err != nil { params := deleteDataLocalizationRegionalHostnameParams(zoneID, rhChange)
if err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, rhChange.hostname, params); err != nil {
changeLog.Errorf("failed to delete regional hostname: %v", err) changeLog.Errorf("failed to delete regional hostname: %v", err)
return false return false
} }
@ -137,18 +156,22 @@ func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, r
return true return true
} }
func (p *CloudFlareProvider) listDataLocalisationRegionalHostnames(ctx context.Context, resourceContainer *cloudflare.ResourceContainer) (regionalHostnamesMap, error) { // listDataLocalisationRegionalHostnames fetches the current regional hostnames for the given zone ID.
rhs, err := p.Client.ListDataLocalizationRegionalHostnames(ctx, resourceContainer, cloudflare.ListDataLocalizationRegionalHostnamesParams{}) //
if err != nil { // It returns a map of hostnames to regional hostnames, or an error if the request fails.
return nil, convertCloudflareError(err) func (p *CloudFlareProvider) listDataLocalisationRegionalHostnames(ctx context.Context, zoneID string) (regionalHostnamesMap, error) {
} params := listDataLocalizationRegionalHostnamesParams(zoneID)
iter := p.Client.ListDataLocalizationRegionalHostnames(ctx, params)
rhsMap := make(regionalHostnamesMap) rhsMap := make(regionalHostnamesMap)
for _, rh := range rhs { for rh := range autoPagerIterator(iter) {
rhsMap[rh.Hostname] = regionalHostname{ rhsMap[rh.Hostname] = regionalHostname{
hostname: rh.Hostname, hostname: rh.Hostname,
regionKey: rh.RegionKey, regionKey: rh.RegionKey,
} }
} }
if iter.Err() != nil {
return nil, convertCloudflareError(iter.Err())
}
return rhsMap, nil return rhsMap, nil
} }
@ -193,7 +216,7 @@ func (p *CloudFlareProvider) addEnpointsProviderSpecificRegionKeyProperty(ctx co
return nil return nil
} }
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, cloudflare.ZoneIdentifier(zoneID)) regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, zoneID)
if err != nil { if err != nil {
return err return err
} }

View File

@ -24,6 +24,7 @@ import (
"testing" "testing"
cloudflare "github.com/cloudflare/cloudflare-go" cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/cloudflare-go/v4/addressing"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -33,61 +34,64 @@ import (
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
) )
func (m *mockCloudFlareClient) ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error) { func (m *mockCloudFlareClient) ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse] {
if strings.Contains(rc.Identifier, "rherror") { zoneID := params.ZoneID.Value
return nil, fmt.Errorf("failed to list regional hostnames") if strings.Contains(zoneID, "rherror") {
return &mockAutoPager[addressing.RegionalHostnameListResponse]{err: fmt.Errorf("failed to list regional hostnames")}
} }
rhs := make([]cloudflare.RegionalHostname, 0, len(m.regionalHostnames[rc.Identifier])) results := make([]addressing.RegionalHostnameListResponse, 0, len(m.regionalHostnames[zoneID]))
for _, rh := range m.regionalHostnames[rc.Identifier] { for _, rh := range m.regionalHostnames[zoneID] {
rhs = append(rhs, cloudflare.RegionalHostname{ results = append(results, addressing.RegionalHostnameListResponse{
Hostname: rh.hostname, Hostname: rh.hostname,
RegionKey: rh.regionKey, RegionKey: rh.regionKey,
}) })
} }
return rhs, nil return &mockAutoPager[addressing.RegionalHostnameListResponse]{
items: results,
}
} }
func (m *mockCloudFlareClient) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error { func (m *mockCloudFlareClient) CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error {
if strings.Contains(rp.Hostname, "rherror") { if strings.Contains(params.Hostname.Value, "rherror") {
return fmt.Errorf("failed to create regional hostname") return fmt.Errorf("failed to create regional hostname")
} }
m.Actions = append(m.Actions, MockAction{ m.Actions = append(m.Actions, MockAction{
Name: "CreateDataLocalizationRegionalHostname", Name: "CreateDataLocalizationRegionalHostname",
ZoneId: rc.Identifier, ZoneId: params.ZoneID.Value,
RecordId: "", RecordId: "",
RegionalHostname: regionalHostname{ RegionalHostname: regionalHostname{
hostname: rp.Hostname, hostname: params.Hostname.Value,
regionKey: rp.RegionKey, regionKey: params.RegionKey.Value,
}, },
}) })
return nil return nil
} }
func (m *mockCloudFlareClient) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error { func (m *mockCloudFlareClient) UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error {
if strings.Contains(rp.Hostname, "rherror") { if strings.Contains(hostname, "rherror") {
return fmt.Errorf("failed to update regional hostname") return fmt.Errorf("failed to update regional hostname")
} }
m.Actions = append(m.Actions, MockAction{ m.Actions = append(m.Actions, MockAction{
Name: "UpdateDataLocalizationRegionalHostname", Name: "UpdateDataLocalizationRegionalHostname",
ZoneId: rc.Identifier, ZoneId: params.ZoneID.Value,
RecordId: "", RecordId: "",
RegionalHostname: regionalHostname{ RegionalHostname: regionalHostname{
hostname: rp.Hostname, hostname: hostname,
regionKey: rp.RegionKey, regionKey: params.RegionKey.Value,
}, },
}) })
return nil return nil
} }
func (m *mockCloudFlareClient) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error { func (m *mockCloudFlareClient) DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error {
if strings.Contains(hostname, "rherror") { if strings.Contains(hostname, "rherror") {
return fmt.Errorf("failed to delete regional hostname") return fmt.Errorf("failed to delete regional hostname")
} }
m.Actions = append(m.Actions, MockAction{ m.Actions = append(m.Actions, MockAction{
Name: "DeleteDataLocalizationRegionalHostname", Name: "DeleteDataLocalizationRegionalHostname",
ZoneId: rc.Identifier, ZoneId: params.ZoneID.Value,
RecordId: "", RecordId: "",
RegionalHostname: regionalHostname{ RegionalHostname: regionalHostname{
hostname: hostname, hostname: hostname,

View File

@ -0,0 +1,34 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cloudflare
type autoPager[T any] interface {
Next() bool
Current() T
Err() error
}
// autoPagerIterator returns an iterator over an autoPager.
func autoPagerIterator[T any](iter autoPager[T]) func(yield func(T) bool) {
return func(yield func(T) bool) {
for iter.Next() {
if !yield(iter.Current()) {
return
}
}
}
}

View File

@ -0,0 +1,94 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cloudflare
import (
"errors"
"slices"
"testing"
"github.com/stretchr/testify/assert"
)
type mockAutoPager[T any] struct {
items []T
index int
err error
errIndex int
}
func (m *mockAutoPager[T]) Next() bool {
m.index++
return !m.hasError() && m.hasNext()
}
func (m *mockAutoPager[T]) Current() T {
if m.hasNext() && !m.hasError() {
return m.items[m.index-1]
}
var zero T
return zero
}
func (m *mockAutoPager[T]) Err() error {
return m.err
}
func (m *mockAutoPager[T]) hasError() bool {
return m.err != nil && m.errIndex <= m.index
}
func (m *mockAutoPager[T]) hasNext() bool {
return m.index > 0 && m.index <= len(m.items)
}
func TestAutoPagerIterator(t *testing.T) {
t.Run("iterate empty", func(t *testing.T) {
pager := &mockAutoPager[string]{}
iterator := autoPagerIterator(pager)
collected := slices.Collect(iterator)
assert.Empty(t, collected)
})
t.Run("iterate all items", func(t *testing.T) {
pager := &mockAutoPager[int]{items: []int{1, 2, 3, 4, 5}}
iterator := autoPagerIterator(pager)
collected := slices.Collect(iterator)
assert.Equal(t, []int{1, 2, 3, 4, 5}, collected)
})
t.Run("iterate with early termination", func(t *testing.T) {
pager := &mockAutoPager[int]{items: []int{1, 2, 3, 4, 5}}
iterator := autoPagerIterator(pager)
var collected []int
for item := range iterator {
collected = append(collected, item)
if item == 3 {
break
}
}
assert.Equal(t, []int{1, 2, 3}, collected)
})
t.Run("iterate with error at index", func(t *testing.T) {
expectedErr := errors.New("pager error")
pager := &mockAutoPager[int]{items: []int{1, 2, 3, 4, 5}, err: expectedErr, errIndex: 3}
iterator := autoPagerIterator(pager)
collected := slices.Collect(iterator)
assert.Equal(t, []int{1, 2}, collected)
})
}