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/civo/civogo v0.6.1
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/datawire/ambassador v1.12.4
github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace
@ -162,6 +163,10 @@ require (
github.com/sosodev/duration v1.3.1 // indirect
github.com/spf13/pflag v1.0.6 // 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/x448/float16 v0.8.4 // 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/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/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/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14=
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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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.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-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550=

View File

@ -29,6 +29,9 @@ import (
"strings"
"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"
"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)
DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) 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)
CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error
UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error
DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error
ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse]
CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error
UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) 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)
DeleteCustomHostname(ctx context.Context, zoneID string, customHostnameID string) error
CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error)
}
type zoneService struct {
service *cloudflare.API
service *cloudflare.API
serviceV4 *cloudflarev4.Client
}
func (z zoneService) ZoneIDByName(zoneName string) (string, error) {
@ -287,8 +291,9 @@ func NewCloudFlareProvider(
) (*CloudFlareProvider, error) {
// initialize via chosen auth method and returns new API object
var (
config *cloudflare.API
err error
config *cloudflare.API
configV4 *cloudflarev4.Client
err error
)
if os.Getenv("CF_API_TOKEN") != "" {
token := os.Getenv("CF_API_TOKEN")
@ -300,8 +305,15 @@ func NewCloudFlareProvider(
token = strings.TrimSpace(string(tokenBytes))
}
config, err = cloudflare.NewWithAPIToken(token)
configV4 = cloudflarev4.NewClient(
option.WithAPIToken(token),
)
} else {
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 {
return nil, fmt.Errorf("failed to initialize cloudflare provider: %w", err)
@ -312,7 +324,7 @@ func NewCloudFlareProvider(
}
return &CloudFlareProvider{
Client: zoneService{config},
Client: zoneService{config, configV4},
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
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)
}
if len(desiredRegionalHostnames) > 0 {
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, resourceContainer)
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, zoneID)
if err != nil {
return fmt.Errorf("could not fetch regional hostnames from zone, %w", err)
}
regionalHostnamesChanges := regionalHostnamesChanges(desiredRegionalHostnames, regionalHostnames)
if !p.submitRegionalHostnameChanges(ctx, regionalHostnamesChanges, resourceContainer) {
if !p.submitRegionalHostnameChanges(ctx, zoneID, regionalHostnamesChanges) {
failedChange = true
}
}

View File

@ -22,7 +22,9 @@ import (
"maps"
"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"
"sigs.k8s.io/external-dns/endpoint"
@ -53,46 +55,62 @@ type regionalHostnameChange struct {
regionalHostname
}
func (z zoneService) ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error) {
return z.service.ListDataLocalizationRegionalHostnames(ctx, rc, rp)
func (z zoneService) ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse] {
return z.serviceV4.Addressing.RegionalHostnames.ListAutoPaging(ctx, params)
}
func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error {
_, err := z.service.CreateDataLocalizationRegionalHostname(ctx, rc, rp)
func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error {
_, err := z.serviceV4.Addressing.RegionalHostnames.New(ctx, params)
return err
}
func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error {
_, err := z.service.UpdateDataLocalizationRegionalHostname(ctx, rc, rp)
func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error {
_, err := z.serviceV4.Addressing.RegionalHostnames.Edit(ctx, hostname, params)
return err
}
func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error {
return z.service.DeleteDataLocalizationRegionalHostname(ctx, rc, hostname)
func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error {
_, 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
func createDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.CreateDataLocalizationRegionalHostnameParams {
return cloudflare.CreateDataLocalizationRegionalHostnameParams{
Hostname: rhc.hostname,
RegionKey: rhc.regionKey,
func createDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameNewParams {
return addressing.RegionalHostnameNewParams{
ZoneID: cloudflarev4.F(zoneID),
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
func updateDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.UpdateDataLocalizationRegionalHostnameParams {
return cloudflare.UpdateDataLocalizationRegionalHostnameParams{
Hostname: rhc.hostname,
RegionKey: rhc.regionKey,
func updateDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameEditParams {
return addressing.RegionalHostnameEditParams{
ZoneID: cloudflarev4.F(zoneID),
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
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
for _, rhChange := range rhChanges {
if !p.submitRegionalHostnameChange(ctx, rhChange, resourceContainer) {
if !p.submitRegionalHostnameChange(ctx, zoneID, rhChange) {
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
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{
"hostname": rhChange.hostname,
"region_key": rhChange.regionKey,
"action": rhChange.action.String(),
"zone": resourceContainer.Identifier,
"zone": zoneID,
})
if p.DryRun {
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 {
case cloudFlareCreate:
changeLog.Debug("Creating regional hostname")
regionalHostnameParam := createDataLocalizationRegionalHostnameParams(rhChange)
if err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam); err != nil {
params := createDataLocalizationRegionalHostnameParams(zoneID, rhChange)
if err := p.Client.CreateDataLocalizationRegionalHostname(ctx, params); err != nil {
changeLog.Errorf("failed to create regional hostname: %v", err)
return false
}
case cloudFlareUpdate:
changeLog.Debug("Updating regional hostname")
regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(rhChange)
if err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam); err != nil {
params := updateDataLocalizationRegionalHostnameParams(zoneID, rhChange)
if err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, rhChange.hostname, params); err != nil {
changeLog.Errorf("failed to update regional hostname: %v", err)
return false
}
case cloudFlareDelete:
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)
return false
}
@ -137,18 +156,22 @@ func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, r
return true
}
func (p *CloudFlareProvider) listDataLocalisationRegionalHostnames(ctx context.Context, resourceContainer *cloudflare.ResourceContainer) (regionalHostnamesMap, error) {
rhs, err := p.Client.ListDataLocalizationRegionalHostnames(ctx, resourceContainer, cloudflare.ListDataLocalizationRegionalHostnamesParams{})
if err != nil {
return nil, convertCloudflareError(err)
}
// listDataLocalisationRegionalHostnames fetches the current regional hostnames for the given zone ID.
//
// It returns a map of hostnames to regional hostnames, or an error if the request fails.
func (p *CloudFlareProvider) listDataLocalisationRegionalHostnames(ctx context.Context, zoneID string) (regionalHostnamesMap, error) {
params := listDataLocalizationRegionalHostnamesParams(zoneID)
iter := p.Client.ListDataLocalizationRegionalHostnames(ctx, params)
rhsMap := make(regionalHostnamesMap)
for _, rh := range rhs {
for rh := range autoPagerIterator(iter) {
rhsMap[rh.Hostname] = regionalHostname{
hostname: rh.Hostname,
regionKey: rh.RegionKey,
}
}
if iter.Err() != nil {
return nil, convertCloudflareError(iter.Err())
}
return rhsMap, nil
}
@ -193,7 +216,7 @@ func (p *CloudFlareProvider) addEnpointsProviderSpecificRegionKeyProperty(ctx co
return nil
}
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, cloudflare.ZoneIdentifier(zoneID))
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, zoneID)
if err != nil {
return err
}

View File

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