mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 17:46:57 +02:00
refactor(cloudflare): use lib v4 for zone services (#5654)
* chore: update zone implementation to v4 * chore: update cloudflare zones to v4 * docs: newline around lists * docs: styling * style: remove trailing whitespace
This commit is contained in:
parent
b741f3103c
commit
23e12c1a09
@ -4,6 +4,37 @@ This tutorial describes how to setup ExternalDNS for usage within a Kubernetes c
|
|||||||
|
|
||||||
Make sure to use **>=0.4.2** version of ExternalDNS for this tutorial.
|
Make sure to use **>=0.4.2** version of ExternalDNS for this tutorial.
|
||||||
|
|
||||||
|
## CloudFlare SDK Migration Status
|
||||||
|
|
||||||
|
ExternalDNS is currently migrating from the legacy CloudFlare Go SDK v0 to the modern v4 SDK to improve performance, reliability, and access to newer CloudFlare features. The migration status is:
|
||||||
|
|
||||||
|
**✅ Fully migrated to v4 SDK:**
|
||||||
|
|
||||||
|
- Zone management (listing, filtering, pagination)
|
||||||
|
- Zone details retrieval (`GetZone`)
|
||||||
|
- Zone ID lookup by name (`ZoneIDByName`)
|
||||||
|
- Zone plan detection (fully v4 implementation)
|
||||||
|
- Regional services (data localization)
|
||||||
|
|
||||||
|
**🔄 Still using legacy v0 SDK:**
|
||||||
|
|
||||||
|
- DNS record management (create, update, delete records)
|
||||||
|
- Custom hostnames
|
||||||
|
- Proxied records
|
||||||
|
|
||||||
|
This mixed approach ensures continued functionality while gradually modernizing the codebase. Users should not experience any breaking changes during this transition.
|
||||||
|
|
||||||
|
### SDK Dependencies
|
||||||
|
|
||||||
|
ExternalDNS currently uses:
|
||||||
|
|
||||||
|
- **cloudflare-go v0.115.0+**: Legacy SDK for DNS records, custom hostnames, and proxied record features
|
||||||
|
- **cloudflare-go/v4 v4.6.0+**: Modern SDK for all zone management and regional services operations
|
||||||
|
|
||||||
|
Zone management has been fully migrated to the v4 SDK, providing improved performance and reliability.
|
||||||
|
|
||||||
|
Both SDKs are automatically managed as Go module dependencies and require no special configuration from users.
|
||||||
|
|
||||||
## Creating a Cloudflare DNS zone
|
## Creating a Cloudflare DNS zone
|
||||||
|
|
||||||
We highly recommend to read this tutorial if you haven't used Cloudflare before:
|
We highly recommend to read this tutorial if you haven't used Cloudflare before:
|
||||||
@ -353,7 +384,7 @@ The custom hostname DNS must resolve to the Cloudflare DNS record (`external-dns
|
|||||||
|
|
||||||
Requires [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/) product and "SSL and Certificates" API permission.
|
Requires [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/) product and "SSL and Certificates" API permission.
|
||||||
|
|
||||||
Due to a limitation within the cloudflare-go v0 API, the custom hostname page size is fixed at 50.
|
**Note:** Due to using the legacy cloudflare-go v0 API for custom hostname management, the custom hostname page size is fixed at 50. This limitation will be addressed in a future migration to the v4 SDK.
|
||||||
|
|
||||||
## Using CRD source to manage DNS records in Cloudflare
|
## Using CRD source to manage DNS records in Cloudflare
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ import (
|
|||||||
cloudflarev4 "github.com/cloudflare/cloudflare-go/v4"
|
cloudflarev4 "github.com/cloudflare/cloudflare-go/v4"
|
||||||
"github.com/cloudflare/cloudflare-go/v4/addressing"
|
"github.com/cloudflare/cloudflare-go/v4/addressing"
|
||||||
"github.com/cloudflare/cloudflare-go/v4/option"
|
"github.com/cloudflare/cloudflare-go/v4/option"
|
||||||
|
"github.com/cloudflare/cloudflare-go/v4/zones"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/publicsuffix"
|
"golang.org/x/net/publicsuffix"
|
||||||
|
|
||||||
@ -106,8 +107,8 @@ var recordTypeCustomHostnameSupported = map[string]bool{
|
|||||||
// cloudFlareDNS is the subset of the CloudFlare API that we actually use. Add methods as required. Signatures must match exactly.
|
// cloudFlareDNS is the subset of the CloudFlare API that we actually use. Add methods as required. Signatures must match exactly.
|
||||||
type cloudFlareDNS interface {
|
type cloudFlareDNS interface {
|
||||||
ZoneIDByName(zoneName string) (string, error)
|
ZoneIDByName(zoneName string) (string, error)
|
||||||
ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error)
|
ListZones(ctx context.Context, params zones.ZoneListParams) autoPager[zones.Zone]
|
||||||
ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error)
|
GetZone(ctx context.Context, zoneID string) (*zones.Zone, error)
|
||||||
ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error)
|
ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error)
|
||||||
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
|
||||||
@ -127,7 +128,23 @@ type zoneService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (z zoneService) ZoneIDByName(zoneName string) (string, error) {
|
func (z zoneService) ZoneIDByName(zoneName string) (string, error) {
|
||||||
return z.service.ZoneIDByName(zoneName)
|
// Use v4 API to find zone by name
|
||||||
|
params := zones.ZoneListParams{
|
||||||
|
Name: cloudflarev4.F(zoneName),
|
||||||
|
}
|
||||||
|
|
||||||
|
iter := z.serviceV4.Zones.ListAutoPaging(context.Background(), params)
|
||||||
|
for zone := range autoPagerIterator(iter) {
|
||||||
|
if zone.Name == zoneName {
|
||||||
|
return zone.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := iter.Err(); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to list zones from CloudFlare API: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("zone %q not found in CloudFlare account - verify the zone exists and API credentials have access to it", zoneName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z zoneService) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) {
|
func (z zoneService) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) {
|
||||||
@ -147,12 +164,12 @@ func (z zoneService) DeleteDNSRecord(ctx context.Context, rc *cloudflare.Resourc
|
|||||||
return z.service.DeleteDNSRecord(ctx, rc, recordID)
|
return z.service.DeleteDNSRecord(ctx, rc, recordID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z zoneService) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
|
func (z zoneService) ListZones(ctx context.Context, params zones.ZoneListParams) autoPager[zones.Zone] {
|
||||||
return z.service.ListZonesContext(ctx, opts...)
|
return z.serviceV4.Zones.ListAutoPaging(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z zoneService) ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error) {
|
func (z zoneService) GetZone(ctx context.Context, zoneID string) (*zones.Zone, error) {
|
||||||
return z.service.ZoneDetails(ctx, zoneID)
|
return z.serviceV4.Zones.Get(ctx, zones.ZoneGetParams{ZoneID: cloudflarev4.F(zoneID)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z zoneService) CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error) {
|
func (z zoneService) CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error) {
|
||||||
@ -167,6 +184,11 @@ func (z zoneService) CreateCustomHostname(ctx context.Context, zoneID string, ch
|
|||||||
return z.service.CreateCustomHostname(ctx, zoneID, ch)
|
return z.service.CreateCustomHostname(ctx, zoneID, ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listZonesV4Params returns the appropriate Zone List Params for v4 API
|
||||||
|
func listZonesV4Params() zones.ZoneListParams {
|
||||||
|
return zones.ZoneListParams{}
|
||||||
|
}
|
||||||
|
|
||||||
type DNSRecordsConfig struct {
|
type DNSRecordsConfig struct {
|
||||||
PerPage int
|
PerPage int
|
||||||
Comment string
|
Comment string
|
||||||
@ -202,13 +224,13 @@ func (p *CloudFlareProvider) ZoneHasPaidPlan(hostname string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
zoneDetails, err := p.Client.ZoneDetails(context.Background(), zoneID)
|
zoneDetails, err := p.Client.GetZone(context.Background(), zoneID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to get zone %s details %v", zone, err)
|
log.Errorf("Failed to get zone %s details %v", zone, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return zoneDetails.Plan.IsSubscribed
|
return zoneDetails.Plan.IsSubscribed //nolint:staticcheck // SA1019: Plan.IsSubscribed is deprecated but no replacement available yet
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
|
// CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
|
||||||
@ -343,8 +365,8 @@ func NewCloudFlareProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Zones returns the list of hosted zones.
|
// Zones returns the list of hosted zones.
|
||||||
func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, error) {
|
func (p *CloudFlareProvider) Zones(ctx context.Context) ([]zones.Zone, error) {
|
||||||
var result []cloudflare.Zone
|
var result []zones.Zone
|
||||||
|
|
||||||
// if there is a zoneIDfilter configured
|
// if there is a zoneIDfilter configured
|
||||||
// && if the filter isn't just a blank string (used in tests)
|
// && if the filter isn't just a blank string (used in tests)
|
||||||
@ -352,34 +374,38 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
|
|||||||
log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
|
log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
|
||||||
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
|
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
|
||||||
log.Debugf("looking up zone %q", zoneID)
|
log.Debugf("looking up zone %q", zoneID)
|
||||||
detailResponse, err := p.Client.ZoneDetails(ctx, zoneID)
|
detailResponse, err := p.Client.GetZone(ctx, zoneID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("zone %q lookup failed, %v", zoneID, err)
|
log.Errorf("zone %q lookup failed, %v", zoneID, err)
|
||||||
return result, err
|
return result, convertCloudflareError(err)
|
||||||
}
|
}
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"zoneName": detailResponse.Name,
|
"zoneName": detailResponse.Name,
|
||||||
"zoneID": detailResponse.ID,
|
"zoneID": detailResponse.ID,
|
||||||
}).Debugln("adding zone for consideration")
|
}).Debugln("adding zone for consideration")
|
||||||
result = append(result, detailResponse)
|
result = append(result, *detailResponse)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugln("no zoneIDFilter configured, looking at all zones")
|
log.Debugln("no zoneIDFilter configured, looking at all zones")
|
||||||
|
|
||||||
zonesResponse, err := p.Client.ListZonesContext(ctx)
|
params := listZonesV4Params()
|
||||||
if err != nil {
|
iter := p.Client.ListZones(ctx, params)
|
||||||
return nil, convertCloudflareError(err)
|
for zone := range autoPagerIterator(iter) {
|
||||||
}
|
|
||||||
|
|
||||||
for _, zone := range zonesResponse.Result {
|
|
||||||
if !p.domainFilter.Match(zone.Name) {
|
if !p.domainFilter.Match(zone.Name) {
|
||||||
log.Debugf("zone %q not in domain filter", zone.Name)
|
log.Debugf("zone %q not in domain filter", zone.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"zoneName": zone.Name,
|
||||||
|
"zoneID": zone.ID,
|
||||||
|
}).Debugln("adding zone for consideration")
|
||||||
result = append(result, zone)
|
result = append(result, zone)
|
||||||
}
|
}
|
||||||
|
if iter.Err() != nil {
|
||||||
|
return nil, convertCloudflareError(iter.Err())
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@ -722,7 +748,7 @@ func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]
|
|||||||
}
|
}
|
||||||
|
|
||||||
// changesByZone separates a multi-zone change into a single change per zone.
|
// changesByZone separates a multi-zone change into a single change per zone.
|
||||||
func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
|
func (p *CloudFlareProvider) changesByZone(zones []zones.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
|
||||||
changes := make(map[string][]*cloudFlareChange)
|
changes := make(map[string][]*cloudFlareChange)
|
||||||
zoneNameIDMapper := provider.ZoneIDName{}
|
zoneNameIDMapper := provider.ZoneIDName{}
|
||||||
|
|
||||||
|
@ -27,9 +27,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflare-go"
|
"github.com/cloudflare/cloudflare-go"
|
||||||
|
"github.com/cloudflare/cloudflare-go/v4/zones"
|
||||||
"github.com/maxatome/go-testdeep/td"
|
"github.com/maxatome/go-testdeep/td"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"sigs.k8s.io/external-dns/endpoint"
|
"sigs.k8s.io/external-dns/endpoint"
|
||||||
"sigs.k8s.io/external-dns/internal/testutils"
|
"sigs.k8s.io/external-dns/internal/testutils"
|
||||||
"sigs.k8s.io/external-dns/plan"
|
"sigs.k8s.io/external-dns/plan"
|
||||||
@ -55,9 +58,8 @@ type mockCloudFlareClient struct {
|
|||||||
Zones map[string]string
|
Zones map[string]string
|
||||||
Records map[string]map[string]cloudflare.DNSRecord
|
Records map[string]map[string]cloudflare.DNSRecord
|
||||||
Actions []MockAction
|
Actions []MockAction
|
||||||
listZonesError error
|
listZonesError error // For v4 ListZones
|
||||||
zoneDetailsError error
|
getZoneError error // For v4 GetZone
|
||||||
listZonesContextError error
|
|
||||||
dnsRecordsError error
|
dnsRecordsError error
|
||||||
customHostnames map[string][]cloudflare.CustomHostname
|
customHostnames map[string][]cloudflare.CustomHostname
|
||||||
regionalHostnames map[string][]regionalHostname
|
regionalHostnames map[string][]regionalHostname
|
||||||
@ -335,54 +337,60 @@ func (m *mockCloudFlareClient) DeleteCustomHostname(ctx context.Context, zoneID
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockCloudFlareClient) ZoneIDByName(zoneName string) (string, error) {
|
func (m *mockCloudFlareClient) ZoneIDByName(zoneName string) (string, error) {
|
||||||
|
// Simulate iterator error (line 144)
|
||||||
|
if m.listZonesError != nil {
|
||||||
|
return "", fmt.Errorf("failed to list zones from CloudFlare API: %w", m.listZonesError)
|
||||||
|
}
|
||||||
|
|
||||||
for id, name := range m.Zones {
|
for id, name := range m.Zones {
|
||||||
if name == zoneName {
|
if name == zoneName {
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", errors.New("Unknown zone: " + zoneName)
|
// Use the improved error message (line 147)
|
||||||
|
return "", fmt.Errorf("zone %q not found in CloudFlare account - verify the zone exists and API credentials have access to it", zoneName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockCloudFlareClient) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
|
// V4 Zone methods
|
||||||
if m.listZonesContextError != nil {
|
func (m *mockCloudFlareClient) ListZones(ctx context.Context, params zones.ZoneListParams) autoPager[zones.Zone] {
|
||||||
return cloudflare.ZonesResponse{}, m.listZonesContextError
|
if m.listZonesError != nil {
|
||||||
|
return &mockAutoPager[zones.Zone]{
|
||||||
|
err: m.listZonesError,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := []cloudflare.Zone{}
|
var results []zones.Zone
|
||||||
|
|
||||||
for zoneId, zoneName := range m.Zones {
|
for id, zoneName := range m.Zones {
|
||||||
result = append(result, cloudflare.Zone{
|
results = append(results, zones.Zone{
|
||||||
ID: zoneId,
|
ID: id,
|
||||||
Name: zoneName,
|
Name: zoneName,
|
||||||
|
Plan: zones.ZonePlan{IsSubscribed: strings.HasSuffix(zoneName, "bar.com")}, //nolint:SA1019 // Plan.IsSubscribed is deprecated but no replacement available yet
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return cloudflare.ZonesResponse{
|
return &mockAutoPager[zones.Zone]{
|
||||||
Result: result,
|
items: results,
|
||||||
ResultInfo: cloudflare.ResultInfo{
|
}
|
||||||
Page: 1,
|
|
||||||
TotalPages: 1,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockCloudFlareClient) ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error) {
|
func (m *mockCloudFlareClient) GetZone(ctx context.Context, zoneID string) (*zones.Zone, error) {
|
||||||
if m.zoneDetailsError != nil {
|
if m.getZoneError != nil {
|
||||||
return cloudflare.Zone{}, m.zoneDetailsError
|
return nil, m.getZoneError
|
||||||
}
|
}
|
||||||
|
|
||||||
for id, zoneName := range m.Zones {
|
for id, zoneName := range m.Zones {
|
||||||
if zoneID == id {
|
if zoneID == id {
|
||||||
return cloudflare.Zone{
|
return &zones.Zone{
|
||||||
ID: zoneID,
|
ID: zoneID,
|
||||||
Name: zoneName,
|
Name: zoneName,
|
||||||
Plan: cloudflare.ZonePlan{IsSubscribed: strings.HasSuffix(zoneName, "bar.com")},
|
Plan: zones.ZonePlan{IsSubscribed: strings.HasSuffix(zoneName, "bar.com")}, //nolint:SA1019 // Plan.IsSubscribed is deprecated but no replacement available yet
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID)
|
return nil, errors.New("Unknown zoneID: " + zoneID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCustomHostnameIdxByID(chs []cloudflare.CustomHostname, customHostnameID string) int {
|
func getCustomHostnameIdxByID(chs []cloudflare.CustomHostname, customHostnameID string) int {
|
||||||
@ -841,7 +849,7 @@ func TestCloudflareZones(t *testing.T) {
|
|||||||
func TestCloudflareZonesFailed(t *testing.T) {
|
func TestCloudflareZonesFailed(t *testing.T) {
|
||||||
|
|
||||||
client := NewMockCloudFlareClient()
|
client := NewMockCloudFlareClient()
|
||||||
client.zoneDetailsError = errors.New("zone lookup failed")
|
client.getZoneError = errors.New("zone lookup failed")
|
||||||
|
|
||||||
provider := &CloudFlareProvider{
|
provider := &CloudFlareProvider{
|
||||||
Client: client,
|
Client: client,
|
||||||
@ -877,7 +885,7 @@ func TestCloudFlareZonesWithIDFilter(t *testing.T) {
|
|||||||
func TestCloudflareListZonesRateLimited(t *testing.T) {
|
func TestCloudflareListZonesRateLimited(t *testing.T) {
|
||||||
// Create a mock client that returns a rate limit error
|
// Create a mock client that returns a rate limit error
|
||||||
client := NewMockCloudFlareClient()
|
client := NewMockCloudFlareClient()
|
||||||
client.listZonesContextError = &cloudflare.Error{
|
client.listZonesError = &cloudflare.Error{
|
||||||
StatusCode: 429,
|
StatusCode: 429,
|
||||||
ErrorCodes: []int{10000},
|
ErrorCodes: []int{10000},
|
||||||
Type: cloudflare.ErrorTypeRateLimit,
|
Type: cloudflare.ErrorTypeRateLimit,
|
||||||
@ -896,7 +904,7 @@ func TestCloudflareListZonesRateLimited(t *testing.T) {
|
|||||||
func TestCloudflareListZonesRateLimitedStringError(t *testing.T) {
|
func TestCloudflareListZonesRateLimitedStringError(t *testing.T) {
|
||||||
// Create a mock client that returns a rate limit error
|
// Create a mock client that returns a rate limit error
|
||||||
client := NewMockCloudFlareClient()
|
client := NewMockCloudFlareClient()
|
||||||
client.listZonesContextError = errors.New("exceeded available rate limit retries")
|
client.listZonesError = errors.New("exceeded available rate limit retries")
|
||||||
p := &CloudFlareProvider{Client: client}
|
p := &CloudFlareProvider{Client: client}
|
||||||
|
|
||||||
// Call the Zones function
|
// Call the Zones function
|
||||||
@ -909,7 +917,7 @@ func TestCloudflareListZonesRateLimitedStringError(t *testing.T) {
|
|||||||
func TestCloudflareListZoneInternalErrors(t *testing.T) {
|
func TestCloudflareListZoneInternalErrors(t *testing.T) {
|
||||||
// Create a mock client that returns a internal server error
|
// Create a mock client that returns a internal server error
|
||||||
client := NewMockCloudFlareClient()
|
client := NewMockCloudFlareClient()
|
||||||
client.listZonesContextError = &cloudflare.Error{
|
client.listZonesError = &cloudflare.Error{
|
||||||
StatusCode: 500,
|
StatusCode: 500,
|
||||||
ErrorCodes: []int{20000},
|
ErrorCodes: []int{20000},
|
||||||
Type: cloudflare.ErrorTypeService,
|
Type: cloudflare.ErrorTypeService,
|
||||||
@ -949,7 +957,7 @@ func TestCloudflareRecords(t *testing.T) {
|
|||||||
t.Errorf("expected to fail")
|
t.Errorf("expected to fail")
|
||||||
}
|
}
|
||||||
client.dnsRecordsError = nil
|
client.dnsRecordsError = nil
|
||||||
client.listZonesContextError = &cloudflare.Error{
|
client.listZonesError = &cloudflare.Error{
|
||||||
StatusCode: 429,
|
StatusCode: 429,
|
||||||
ErrorCodes: []int{10000},
|
ErrorCodes: []int{10000},
|
||||||
Type: cloudflare.ErrorTypeRateLimit,
|
Type: cloudflare.ErrorTypeRateLimit,
|
||||||
@ -960,7 +968,7 @@ func TestCloudflareRecords(t *testing.T) {
|
|||||||
t.Error("expected a rate limit error")
|
t.Error("expected a rate limit error")
|
||||||
}
|
}
|
||||||
|
|
||||||
client.listZonesContextError = &cloudflare.Error{
|
client.listZonesError = &cloudflare.Error{
|
||||||
StatusCode: 500,
|
StatusCode: 500,
|
||||||
ErrorCodes: []int{10000},
|
ErrorCodes: []int{10000},
|
||||||
Type: cloudflare.ErrorTypeService,
|
Type: cloudflare.ErrorTypeService,
|
||||||
@ -971,7 +979,7 @@ func TestCloudflareRecords(t *testing.T) {
|
|||||||
t.Error("expected a internal server error")
|
t.Error("expected a internal server error")
|
||||||
}
|
}
|
||||||
|
|
||||||
client.listZonesContextError = errors.New("failed to list zones")
|
client.listZonesError = errors.New("failed to list zones")
|
||||||
_, err = p.Records(ctx)
|
_, err = p.Records(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("expected to fail")
|
t.Errorf("expected to fail")
|
||||||
@ -2584,7 +2592,7 @@ func TestZoneHasPaidPlan(t *testing.T) {
|
|||||||
assert.True(t, cfprovider.ZoneHasPaidPlan("subdomain.bar.com"))
|
assert.True(t, cfprovider.ZoneHasPaidPlan("subdomain.bar.com"))
|
||||||
assert.False(t, cfprovider.ZoneHasPaidPlan("invaliddomain"))
|
assert.False(t, cfprovider.ZoneHasPaidPlan("invaliddomain"))
|
||||||
|
|
||||||
client.zoneDetailsError = errors.New("zone lookup failed")
|
client.getZoneError = errors.New("zone lookup failed")
|
||||||
cfproviderWithZoneError := &CloudFlareProvider{
|
cfproviderWithZoneError := &CloudFlareProvider{
|
||||||
Client: client,
|
Client: client,
|
||||||
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||||
@ -2592,6 +2600,7 @@ func TestZoneHasPaidPlan(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.False(t, cfproviderWithZoneError.ZoneHasPaidPlan("subdomain.foo.com"))
|
assert.False(t, cfproviderWithZoneError.ZoneHasPaidPlan("subdomain.foo.com"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudflareApplyChanges_AllErrorLogPaths(t *testing.T) {
|
func TestCloudflareApplyChanges_AllErrorLogPaths(t *testing.T) {
|
||||||
hook := testutils.LogsUnderTestWithLogLevel(log.ErrorLevel, t)
|
hook := testutils.LogsUnderTestWithLogLevel(log.ErrorLevel, t)
|
||||||
|
|
||||||
@ -2760,3 +2769,488 @@ func TestCloudFlareProvider_SupportedAdditionalRecordTypes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCloudflareZoneChanges(t *testing.T) {
|
||||||
|
client := NewMockCloudFlareClient()
|
||||||
|
cfProvider := &CloudFlareProvider{
|
||||||
|
Client: client,
|
||||||
|
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||||
|
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test zone listing and filtering
|
||||||
|
zones, err := cfProvider.Zones(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, zones, 2)
|
||||||
|
|
||||||
|
// Verify zone names
|
||||||
|
zoneNames := make([]string, len(zones))
|
||||||
|
for i, zone := range zones {
|
||||||
|
zoneNames[i] = zone.Name
|
||||||
|
}
|
||||||
|
assert.Contains(t, zoneNames, "foo.com")
|
||||||
|
assert.Contains(t, zoneNames, "bar.com")
|
||||||
|
|
||||||
|
// Test zone filtering with specific zone ID
|
||||||
|
providerWithZoneFilter := &CloudFlareProvider{
|
||||||
|
Client: client,
|
||||||
|
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||||
|
zoneIDFilter: provider.NewZoneIDFilter([]string{"001"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredZones, err := providerWithZoneFilter.Zones(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, filteredZones, 1)
|
||||||
|
assert.Equal(t, "bar.com", filteredZones[0].Name) // zone 001 is bar.com
|
||||||
|
assert.Equal(t, "001", filteredZones[0].ID)
|
||||||
|
|
||||||
|
// Test zone changes grouping
|
||||||
|
changes := []*cloudFlareChange{
|
||||||
|
{
|
||||||
|
Action: cloudFlareCreate,
|
||||||
|
ResourceRecord: cloudflare.DNSRecord{Name: "test1.foo.com", Type: "A", Content: "1.2.3.4"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: cloudFlareCreate,
|
||||||
|
ResourceRecord: cloudflare.DNSRecord{Name: "test2.foo.com", Type: "A", Content: "1.2.3.5"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: cloudFlareCreate,
|
||||||
|
ResourceRecord: cloudflare.DNSRecord{Name: "test1.bar.com", Type: "A", Content: "1.2.3.6"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
changesByZone := cfProvider.changesByZone(zones, changes)
|
||||||
|
assert.Len(t, changesByZone, 2)
|
||||||
|
assert.Len(t, changesByZone["001"], 1) // bar.com zone (test1.bar.com)
|
||||||
|
assert.Len(t, changesByZone["002"], 2) // foo.com zone (test1.foo.com, test2.foo.com)
|
||||||
|
|
||||||
|
// Test paid plan detection
|
||||||
|
assert.False(t, cfProvider.ZoneHasPaidPlan("subdomain.foo.com")) // free plan
|
||||||
|
assert.True(t, cfProvider.ZoneHasPaidPlan("subdomain.bar.com")) // paid plan
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudflareZoneErrors(t *testing.T) {
|
||||||
|
client := NewMockCloudFlareClient()
|
||||||
|
|
||||||
|
// Test list zones error
|
||||||
|
client.listZonesError = errors.New("failed to list zones")
|
||||||
|
cfProvider := &CloudFlareProvider{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
zones, err := cfProvider.Zones(context.Background())
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to list zones")
|
||||||
|
assert.Nil(t, zones)
|
||||||
|
|
||||||
|
// Test get zone error
|
||||||
|
client.listZonesError = nil
|
||||||
|
client.getZoneError = errors.New("failed to get zone")
|
||||||
|
|
||||||
|
// This should still work for listing but fail when getting individual zones
|
||||||
|
zones, err = cfProvider.Zones(context.Background())
|
||||||
|
assert.NoError(t, err) // List works, individual gets may fail internally
|
||||||
|
assert.NotNil(t, zones)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudflareZoneFiltering(t *testing.T) {
|
||||||
|
client := NewMockCloudFlareClient()
|
||||||
|
|
||||||
|
// Test with domain filter only
|
||||||
|
cfProvider := &CloudFlareProvider{
|
||||||
|
Client: client,
|
||||||
|
domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}),
|
||||||
|
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
|
||||||
|
}
|
||||||
|
|
||||||
|
zones, err := cfProvider.Zones(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, zones, 1)
|
||||||
|
assert.Equal(t, "foo.com", zones[0].Name)
|
||||||
|
|
||||||
|
// Test with zone ID filter
|
||||||
|
providerWithIDFilter := &CloudFlareProvider{
|
||||||
|
Client: client,
|
||||||
|
domainFilter: endpoint.NewDomainFilter([]string{}),
|
||||||
|
zoneIDFilter: provider.NewZoneIDFilter([]string{"002"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredZones, err := providerWithIDFilter.Zones(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, filteredZones, 1)
|
||||||
|
assert.Equal(t, "foo.com", filteredZones[0].Name) // zone 002 is foo.com
|
||||||
|
assert.Equal(t, "002", filteredZones[0].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudflareZonePlanDetection(t *testing.T) {
|
||||||
|
client := NewMockCloudFlareClient()
|
||||||
|
cfProvider := &CloudFlareProvider{
|
||||||
|
Client: client,
|
||||||
|
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||||
|
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test free plan detection (foo.com)
|
||||||
|
assert.False(t, cfProvider.ZoneHasPaidPlan("foo.com"))
|
||||||
|
assert.False(t, cfProvider.ZoneHasPaidPlan("subdomain.foo.com"))
|
||||||
|
assert.False(t, cfProvider.ZoneHasPaidPlan("deep.subdomain.foo.com"))
|
||||||
|
|
||||||
|
// Test paid plan detection (bar.com)
|
||||||
|
assert.True(t, cfProvider.ZoneHasPaidPlan("bar.com"))
|
||||||
|
assert.True(t, cfProvider.ZoneHasPaidPlan("subdomain.bar.com"))
|
||||||
|
assert.True(t, cfProvider.ZoneHasPaidPlan("deep.subdomain.bar.com"))
|
||||||
|
|
||||||
|
// Test invalid domain
|
||||||
|
assert.False(t, cfProvider.ZoneHasPaidPlan("invalid.domain.com"))
|
||||||
|
|
||||||
|
// Test with zone error
|
||||||
|
client.getZoneError = errors.New("zone lookup failed")
|
||||||
|
providerWithError := &CloudFlareProvider{
|
||||||
|
Client: client,
|
||||||
|
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||||
|
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
|
||||||
|
}
|
||||||
|
assert.False(t, providerWithError.ZoneHasPaidPlan("subdomain.foo.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudflareChangesByZone(t *testing.T) {
|
||||||
|
client := NewMockCloudFlareClient()
|
||||||
|
cfProvider := &CloudFlareProvider{
|
||||||
|
Client: client,
|
||||||
|
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||||
|
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
|
||||||
|
}
|
||||||
|
|
||||||
|
zones, err := cfProvider.Zones(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, zones, 2)
|
||||||
|
|
||||||
|
// Test empty changes
|
||||||
|
emptyChanges := []*cloudFlareChange{}
|
||||||
|
changesByZone := cfProvider.changesByZone(zones, emptyChanges)
|
||||||
|
assert.Len(t, changesByZone, 2) // Should return map with zones but empty slices
|
||||||
|
assert.Empty(t, changesByZone["001"]) // bar.com zone should have no changes
|
||||||
|
assert.Empty(t, changesByZone["002"]) // foo.com zone should have no changes
|
||||||
|
|
||||||
|
// Test changes for different zones
|
||||||
|
changes := []*cloudFlareChange{
|
||||||
|
{
|
||||||
|
Action: cloudFlareCreate,
|
||||||
|
ResourceRecord: cloudflare.DNSRecord{Name: "api.foo.com", Type: "A", Content: "1.2.3.4"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: cloudFlareUpdate,
|
||||||
|
ResourceRecord: cloudflare.DNSRecord{Name: "www.foo.com", Type: "CNAME", Content: "foo.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: cloudFlareCreate,
|
||||||
|
ResourceRecord: cloudflare.DNSRecord{Name: "mail.bar.com", Type: "MX", Content: "10 mail.bar.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: cloudFlareDelete,
|
||||||
|
ResourceRecord: cloudflare.DNSRecord{Name: "old.bar.com", Type: "A", Content: "5.6.7.8"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
changesByZone = cfProvider.changesByZone(zones, changes)
|
||||||
|
assert.Len(t, changesByZone, 2)
|
||||||
|
|
||||||
|
// Verify bar.com zone changes (zone 001)
|
||||||
|
barChanges := changesByZone["001"]
|
||||||
|
assert.Len(t, barChanges, 2)
|
||||||
|
assert.Equal(t, "mail.bar.com", barChanges[0].ResourceRecord.Name)
|
||||||
|
assert.Equal(t, "old.bar.com", barChanges[1].ResourceRecord.Name)
|
||||||
|
|
||||||
|
// Verify foo.com zone changes (zone 002)
|
||||||
|
fooChanges := changesByZone["002"]
|
||||||
|
assert.Len(t, fooChanges, 2)
|
||||||
|
assert.Equal(t, "api.foo.com", fooChanges[0].ResourceRecord.Name)
|
||||||
|
assert.Equal(t, "www.foo.com", fooChanges[1].ResourceRecord.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertCloudflareError(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputError error
|
||||||
|
expectSoftError bool
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Rate limit error via Error type",
|
||||||
|
inputError: &cloudflare.Error{StatusCode: 429, Type: cloudflare.ErrorTypeRateLimit},
|
||||||
|
expectSoftError: true,
|
||||||
|
description: "CloudFlare API rate limit error should be converted to soft error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Rate limit error via ClientRateLimited",
|
||||||
|
inputError: &cloudflare.Error{StatusCode: 429, ErrorCodes: []int{10000}, Type: cloudflare.ErrorTypeRateLimit}, // Complete rate limit error
|
||||||
|
expectSoftError: true,
|
||||||
|
description: "CloudFlare client rate limited error should be converted to soft error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Server error 500",
|
||||||
|
inputError: &cloudflare.Error{StatusCode: 500},
|
||||||
|
expectSoftError: true,
|
||||||
|
description: "Server error (500+) should be converted to soft error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Server error 502",
|
||||||
|
inputError: &cloudflare.Error{StatusCode: 502},
|
||||||
|
expectSoftError: true,
|
||||||
|
description: "Server error (502) should be converted to soft error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Server error 503",
|
||||||
|
inputError: &cloudflare.Error{StatusCode: 503},
|
||||||
|
expectSoftError: true,
|
||||||
|
description: "Server error (503) should be converted to soft error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Rate limit string error",
|
||||||
|
inputError: errors.New("exceeded available rate limit retries"),
|
||||||
|
expectSoftError: true,
|
||||||
|
description: "String error containing rate limit message should be converted to soft error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Rate limit string error mixed case",
|
||||||
|
inputError: errors.New("request failed: exceeded available rate limit retries for this operation"),
|
||||||
|
expectSoftError: true,
|
||||||
|
description: "String error containing rate limit message should be converted to soft error regardless of context",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Client error 400",
|
||||||
|
inputError: &cloudflare.Error{StatusCode: 400},
|
||||||
|
expectSoftError: false,
|
||||||
|
description: "Client error (400) should not be converted to soft error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Client error 401",
|
||||||
|
inputError: &cloudflare.Error{StatusCode: 401},
|
||||||
|
expectSoftError: false,
|
||||||
|
description: "Client error (401) should not be converted to soft error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Client error 404",
|
||||||
|
inputError: &cloudflare.Error{StatusCode: 404},
|
||||||
|
expectSoftError: false,
|
||||||
|
description: "Client error (404) should not be converted to soft error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Generic error",
|
||||||
|
inputError: errors.New("some generic error"),
|
||||||
|
expectSoftError: false,
|
||||||
|
description: "Generic error should not be converted to soft error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Network error",
|
||||||
|
inputError: errors.New("connection refused"),
|
||||||
|
expectSoftError: false,
|
||||||
|
description: "Network error should not be converted to soft error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := convertCloudflareError(tt.inputError)
|
||||||
|
|
||||||
|
if tt.expectSoftError {
|
||||||
|
assert.ErrorIs(t, result, provider.SoftError,
|
||||||
|
"Expected soft error for %s: %s", tt.name, tt.description)
|
||||||
|
|
||||||
|
// Verify the original error message is preserved in the soft error
|
||||||
|
assert.Contains(t, result.Error(), tt.inputError.Error(),
|
||||||
|
"Original error message should be preserved")
|
||||||
|
} else {
|
||||||
|
assert.NotErrorIs(t, result, provider.SoftError,
|
||||||
|
"Expected non-soft error for %s: %s", tt.name, tt.description)
|
||||||
|
assert.Equal(t, tt.inputError, result,
|
||||||
|
"Non-soft errors should be returned unchanged")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertCloudflareErrorInContext(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupMock func(*mockCloudFlareClient)
|
||||||
|
function func(*CloudFlareProvider) error
|
||||||
|
expectSoftError bool
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Zones with GetZone rate limit error",
|
||||||
|
setupMock: func(client *mockCloudFlareClient) {
|
||||||
|
client.Zones = map[string]string{"zone1": "example.com"}
|
||||||
|
client.getZoneError = &cloudflare.Error{StatusCode: 429, Type: cloudflare.ErrorTypeRateLimit}
|
||||||
|
},
|
||||||
|
function: func(p *CloudFlareProvider) error {
|
||||||
|
p.zoneIDFilter.ZoneIDs = []string{"zone1"}
|
||||||
|
_, err := p.Zones(context.Background())
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
expectSoftError: true,
|
||||||
|
description: "Zones function should convert GetZone rate limit errors to soft errors",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Zones with GetZone server error",
|
||||||
|
setupMock: func(client *mockCloudFlareClient) {
|
||||||
|
client.Zones = map[string]string{"zone1": "example.com"}
|
||||||
|
client.getZoneError = &cloudflare.Error{StatusCode: 500}
|
||||||
|
},
|
||||||
|
function: func(p *CloudFlareProvider) error {
|
||||||
|
p.zoneIDFilter.ZoneIDs = []string{"zone1"}
|
||||||
|
_, err := p.Zones(context.Background())
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
expectSoftError: true,
|
||||||
|
description: "Zones function should convert GetZone server errors to soft errors",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Zones with GetZone client error",
|
||||||
|
setupMock: func(client *mockCloudFlareClient) {
|
||||||
|
client.Zones = map[string]string{"zone1": "example.com"}
|
||||||
|
client.getZoneError = &cloudflare.Error{StatusCode: 404}
|
||||||
|
},
|
||||||
|
function: func(p *CloudFlareProvider) error {
|
||||||
|
p.zoneIDFilter.ZoneIDs = []string{"zone1"}
|
||||||
|
_, err := p.Zones(context.Background())
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
expectSoftError: false,
|
||||||
|
description: "Zones function should not convert GetZone client errors to soft errors",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Zones with ListZones rate limit error",
|
||||||
|
setupMock: func(client *mockCloudFlareClient) {
|
||||||
|
client.listZonesError = errors.New("exceeded available rate limit retries")
|
||||||
|
},
|
||||||
|
function: func(p *CloudFlareProvider) error {
|
||||||
|
_, err := p.Zones(context.Background())
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
expectSoftError: true,
|
||||||
|
description: "Zones function should convert ListZones rate limit string errors to soft errors",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Zones with ListZones server error",
|
||||||
|
setupMock: func(client *mockCloudFlareClient) {
|
||||||
|
client.listZonesError = &cloudflare.Error{StatusCode: 503}
|
||||||
|
},
|
||||||
|
function: func(p *CloudFlareProvider) error {
|
||||||
|
_, err := p.Zones(context.Background())
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
expectSoftError: true,
|
||||||
|
description: "Zones function should convert ListZones server errors to soft errors",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client := NewMockCloudFlareClient()
|
||||||
|
tt.setupMock(client)
|
||||||
|
|
||||||
|
p := &CloudFlareProvider{
|
||||||
|
Client: client,
|
||||||
|
zoneIDFilter: provider.ZoneIDFilter{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tt.function(p)
|
||||||
|
assert.Error(t, err, "Expected an error from %s", tt.name)
|
||||||
|
|
||||||
|
if tt.expectSoftError {
|
||||||
|
assert.ErrorIs(t, err, provider.SoftError,
|
||||||
|
"Expected soft error for %s: %s", tt.name, tt.description)
|
||||||
|
} else {
|
||||||
|
assert.NotErrorIs(t, err, provider.SoftError,
|
||||||
|
"Expected non-soft error for %s: %s", tt.name, tt.description)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudFlareZonesDomainFilter(t *testing.T) {
|
||||||
|
// Set required environment variables for CloudFlare provider
|
||||||
|
t.Setenv("CF_API_TOKEN", "test-token")
|
||||||
|
|
||||||
|
client := NewMockCloudFlareClient()
|
||||||
|
|
||||||
|
// Create a domain filter that only matches "bar.com"
|
||||||
|
// This should filter out "foo.com" and trigger the debug log
|
||||||
|
domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
|
||||||
|
|
||||||
|
p, err := NewCloudFlareProvider(
|
||||||
|
domainFilter,
|
||||||
|
provider.NewZoneIDFilter([]string{""}), // empty zone ID filter so it uses ListZones path
|
||||||
|
false, // proxied
|
||||||
|
false, // dry run
|
||||||
|
RegionalServicesConfig{},
|
||||||
|
CustomHostnamesConfig{},
|
||||||
|
DNSRecordsConfig{PerPage: 50},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Replace the real client with our mock
|
||||||
|
p.Client = client
|
||||||
|
|
||||||
|
// Capture debug logs to verify the filter log message
|
||||||
|
oldLevel := log.GetLevel()
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
defer log.SetLevel(oldLevel)
|
||||||
|
|
||||||
|
// Use a custom formatter to capture log output
|
||||||
|
var logOutput strings.Builder
|
||||||
|
log.SetOutput(&logOutput)
|
||||||
|
defer log.SetOutput(os.Stderr)
|
||||||
|
|
||||||
|
// Call Zones() which should trigger the domain filter logic
|
||||||
|
zones, err := p.Zones(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Should only return the "bar.com" zone since "foo.com" is filtered out
|
||||||
|
assert.Len(t, zones, 1)
|
||||||
|
assert.Equal(t, "bar.com", zones[0].Name)
|
||||||
|
assert.Equal(t, "001", zones[0].ID)
|
||||||
|
|
||||||
|
// Verify that the debug log was written for the filtered zone
|
||||||
|
logString := logOutput.String()
|
||||||
|
assert.Contains(t, logString, `zone \"foo.com\" not in domain filter`)
|
||||||
|
assert.Contains(t, logString, "no zoneIDFilter configured, looking at all zones")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZoneIDByNameIteratorError(t *testing.T) {
|
||||||
|
client := NewMockCloudFlareClient()
|
||||||
|
|
||||||
|
// Set up an error that will be returned by the ListZones iterator (line 144)
|
||||||
|
client.listZonesError = fmt.Errorf("CloudFlare API connection timeout")
|
||||||
|
|
||||||
|
// Call ZoneIDByName which should hit line 144 (iterator error handling)
|
||||||
|
zoneID, err := client.ZoneIDByName("example.com")
|
||||||
|
|
||||||
|
// Should return empty zone ID and the wrapped iterator error
|
||||||
|
assert.Empty(t, zoneID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to list zones from CloudFlare API")
|
||||||
|
assert.Contains(t, err.Error(), "CloudFlare API connection timeout")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZoneIDByNameZoneNotFound(t *testing.T) {
|
||||||
|
client := NewMockCloudFlareClient()
|
||||||
|
|
||||||
|
// Set up mock to return different zones but not the one we're looking for
|
||||||
|
client.Zones = map[string]string{
|
||||||
|
"zone456": "different.com",
|
||||||
|
"zone789": "another.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call ZoneIDByName for a zone that doesn't exist, should hit line 147 (zone not found)
|
||||||
|
zoneID, err := client.ZoneIDByName("nonexistent.com")
|
||||||
|
|
||||||
|
// Should return empty zone ID and the improved error message
|
||||||
|
assert.Empty(t, zoneID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), `zone "nonexistent.com" not found in CloudFlare account`)
|
||||||
|
assert.Contains(t, err.Error(), "verify the zone exists and API credentials have access to it")
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user