Skip to content

Commit 017f768

Browse files
authored
fix(cloudflare): infinite loop with more than 50 custom hostnames (#5181)
* bugfix - do not reset the resultInfo var, causing infinite loop when number of custom hostnames more than 50 * support paging for custom hostnames tests; update doc
1 parent 69da80f commit 017f768

File tree

3 files changed

+114
-27
lines changed

3 files changed

+114
-27
lines changed

docs/tutorials/cloudflare.md

+2
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ The custom hostname DNS must resolve to the Cloudflare DNS record (`external-dns
320320

321321
Requires [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/) product and "SSL and Certificates" API permission.
322322

323+
Due to a limitation within the cloudflare-go v0 API, the custom hostname page size is fixed at 50.
324+
323325
## Using CRD source to manage DNS records in Cloudflare
324326

325327
Please refer to the [CRD source documentation](../sources/crd.md#example) for more information.

provider/cloudflare/cloudflare.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte
642642
var chs []cloudflare.CustomHostname
643643
resultInfo := cloudflare.ResultInfo{Page: 1}
644644
for {
645-
pageCustomHostnameListResponse, resultInfo, err := p.Client.CustomHostnames(ctx, zoneID, resultInfo.Page, cloudflare.CustomHostname{})
645+
pageCustomHostnameListResponse, result, err := p.Client.CustomHostnames(ctx, zoneID, resultInfo.Page, cloudflare.CustomHostname{})
646646
if err != nil {
647647
var apiErr *cloudflare.Error
648648
if errors.As(err, &apiErr) {
@@ -656,7 +656,7 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte
656656
}
657657

658658
chs = append(chs, pageCustomHostnameListResponse...)
659-
resultInfo = resultInfo.Next()
659+
resultInfo = result.Next()
660660
if resultInfo.Done() {
661661
break
662662
}

provider/cloudflare/cloudflare_test.go

+110-25
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type mockCloudFlareClient struct {
5151
listZonesError error
5252
listZonesContextError error
5353
dnsRecordsError error
54-
customHostnames map[string]map[string]cloudflare.CustomHostname
54+
customHostnames map[string][]cloudflare.CustomHostname
5555
}
5656

5757
var ExampleDomain = []cloudflare.DNSRecord{
@@ -92,7 +92,7 @@ func NewMockCloudFlareClient() *mockCloudFlareClient {
9292
"001": {},
9393
"002": {},
9494
},
95-
customHostnames: map[string]map[string]cloudflare.CustomHostname{},
95+
customHostnames: map[string][]cloudflare.CustomHostname{},
9696
}
9797
}
9898

@@ -261,55 +261,71 @@ func (m *mockCloudFlareClient) UserDetails(ctx context.Context) (cloudflare.User
261261

262262
func (m *mockCloudFlareClient) CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error) {
263263
var err error = nil
264+
perPage := 50 // cloudflare-go v0 API hardcoded
264265

265266
if strings.HasPrefix(zoneID, "newerror-") {
266267
return nil, cloudflare.ResultInfo{}, errors.New("failed to list custom hostnames")
267268
}
268-
269-
if page != 1 || filter.Hostname != "" {
270-
err = errors.New("pages and filters are not supported for custom hostnames mock test")
269+
if filter.Hostname != "" {
270+
err = errors.New("filters are not supported for custom hostnames mock test")
271+
return nil, cloudflare.ResultInfo{}, err
272+
}
273+
if page < 1 {
274+
err = errors.New("incorrect page value for custom hostnames list")
275+
return nil, cloudflare.ResultInfo{}, err
271276
}
272277

273278
result := []cloudflare.CustomHostname{}
274-
if zone, ok := m.customHostnames[zoneID]; ok {
275-
for _, ch := range zone {
279+
if chs, ok := m.customHostnames[zoneID]; ok {
280+
for idx := (page - 1) * perPage; idx < min(len(chs), page*perPage); idx++ {
281+
ch := m.customHostnames[zoneID][idx]
276282
if strings.HasPrefix(ch.Hostname, "newerror-list-") {
277283
m.DeleteCustomHostname(ctx, zoneID, ch.ID)
278284
return nil, cloudflare.ResultInfo{}, errors.New("failed to list erroring custom hostname")
279285
}
280286
result = append(result, ch)
281287
}
288+
return result,
289+
cloudflare.ResultInfo{
290+
Page: page,
291+
PerPage: perPage,
292+
Count: len(result),
293+
Total: len(chs),
294+
TotalPages: len(chs)/page + 1,
295+
}, err
296+
} else {
297+
return result,
298+
cloudflare.ResultInfo{
299+
Page: page,
300+
PerPage: perPage,
301+
Count: 0,
302+
Total: 0,
303+
TotalPages: 0,
304+
}, err
282305
}
283-
284-
return result,
285-
cloudflare.ResultInfo{
286-
Page: 1,
287-
PerPage: 100,
288-
Count: len(result),
289-
Total: len(result),
290-
TotalPages: 1,
291-
}, err
292306
}
293307

294308
func (m *mockCloudFlareClient) CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error) {
295309
if ch.Hostname == "" || ch.CustomOriginServer == "" || ch.Hostname == "newerror-create.foo.fancybar.com" {
296310
return nil, fmt.Errorf("Invalid custom hostname or origin hostname")
297311
}
298312
if _, ok := m.customHostnames[zoneID]; !ok {
299-
m.customHostnames[zoneID] = map[string]cloudflare.CustomHostname{}
313+
m.customHostnames[zoneID] = []cloudflare.CustomHostname{}
300314
}
301315
var newCustomHostname cloudflare.CustomHostname = ch
302316
newCustomHostname.ID = fmt.Sprintf("ID-%s", ch.Hostname)
303-
m.customHostnames[zoneID][newCustomHostname.ID] = newCustomHostname
317+
m.customHostnames[zoneID] = append(m.customHostnames[zoneID], newCustomHostname)
304318
return &cloudflare.CustomHostnameResponse{}, nil
305319
}
306320

307321
func (m *mockCloudFlareClient) DeleteCustomHostname(ctx context.Context, zoneID string, customHostnameID string) error {
308-
if zone, ok := m.customHostnames[zoneID]; ok {
309-
if _, ok := zone[customHostnameID]; ok {
310-
delete(zone, customHostnameID)
311-
}
322+
idx := 0
323+
if idx = getCustomHostnameIdxByID(m.customHostnames[zoneID], customHostnameID); idx < 0 {
324+
return fmt.Errorf("Invalid custom hostname ID to delete")
312325
}
326+
327+
m.customHostnames[zoneID] = append(m.customHostnames[zoneID][:idx], m.customHostnames[zoneID][idx+1:]...)
328+
313329
if customHostnameID == "ID-newerror-delete.foo.fancybar.com" {
314330
return fmt.Errorf("Invalid custom hostname to delete")
315331
}
@@ -379,6 +395,15 @@ func (m *mockCloudFlareClient) ZoneDetails(ctx context.Context, zoneID string) (
379395
return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID)
380396
}
381397

398+
func getCustomHostnameIdxByID(chs []cloudflare.CustomHostname, customHostnameID string) int {
399+
for idx, ch := range chs {
400+
if ch.ID == customHostnameID {
401+
return idx
402+
}
403+
}
404+
return -1
405+
}
406+
382407
func (p *CloudFlareProvider) getCustomHostnameIDbyCustomHostnameAndOrigin(chs []cloudflare.CustomHostname, customHostname string, origin string) (string, string) {
383408
for _, zoneCh := range chs {
384409
if zoneCh.Hostname == customHostname && zoneCh.CustomOriginServer == origin {
@@ -1720,7 +1745,7 @@ func TestCloudflareZoneRecordsFail(t *testing.T) {
17201745
"newerror-001": "bar.com",
17211746
},
17221747
Records: map[string]map[string]cloudflare.DNSRecord{},
1723-
customHostnames: map[string]map[string]cloudflare.CustomHostname{},
1748+
customHostnames: map[string][]cloudflare.CustomHostname{},
17241749
}
17251750
failingProvider := &CloudFlareProvider{
17261751
Client: client,
@@ -2261,13 +2286,14 @@ func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) {
22612286
chID, _ := provider.getCustomHostnameOrigin(chs, "newerror-getCustomHostnameOrigin.foo.fancybar.com")
22622287
if chID != "" {
22632288
t.Logf("corrupting custom hostname %v", chID)
2264-
oldCh := client.customHostnames[zoneID][chID]
2289+
oldIdx := getCustomHostnameIdxByID(client.customHostnames[zoneID], chID)
2290+
oldCh := client.customHostnames[zoneID][oldIdx]
22652291
ch := cloudflare.CustomHostname{
22662292
Hostname: "corrupted-newerror-getCustomHostnameOrigin.foo.fancybar.com",
22672293
CustomOriginServer: oldCh.CustomOriginServer,
22682294
SSL: oldCh.SSL,
22692295
}
2270-
client.customHostnames[zoneID][chID] = ch
2296+
client.customHostnames[zoneID][oldIdx] = ch
22712297
}
22722298
}
22732299

@@ -2278,3 +2304,62 @@ func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) {
22782304
}
22792305
assert.Contains(t, b.String(), "level=info msg=\"Custom hostname newerror-getCustomHostnameOrigin.foo.fancybar.com not found\" action=DELETE record=create.foo.bar.com")
22802306
}
2307+
2308+
func TestCloudflareListCustomHostnamesWithPagionation(t *testing.T) {
2309+
client := NewMockCloudFlareClient()
2310+
provider := &CloudFlareProvider{
2311+
Client: client,
2312+
CustomHostnamesConfig: CustomHostnamesConfig{Enabled: true},
2313+
}
2314+
ctx := context.Background()
2315+
domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
2316+
2317+
const CustomHostnamesNumber = 342
2318+
var generatedEndpoints []*endpoint.Endpoint
2319+
for i := 0; i < CustomHostnamesNumber; i++ {
2320+
ep := []*endpoint.Endpoint{
2321+
{
2322+
DNSName: fmt.Sprintf("host-%d.foo.bar.com", i),
2323+
Targets: endpoint.Targets{fmt.Sprintf("cname-%d.foo.bar.com", i)},
2324+
RecordType: endpoint.RecordTypeCNAME,
2325+
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
2326+
Labels: endpoint.Labels{},
2327+
ProviderSpecific: endpoint.ProviderSpecific{
2328+
{
2329+
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
2330+
Value: fmt.Sprintf("host-%d.foo.fancybar.com", i),
2331+
},
2332+
},
2333+
},
2334+
}
2335+
generatedEndpoints = append(generatedEndpoints, ep...)
2336+
}
2337+
2338+
records, err := provider.Records(ctx)
2339+
if err != nil {
2340+
t.Errorf("should not fail, %s", err)
2341+
}
2342+
2343+
endpoints, err := provider.AdjustEndpoints(generatedEndpoints)
2344+
2345+
assert.NoError(t, err)
2346+
plan := &plan.Plan{
2347+
Current: records,
2348+
Desired: endpoints,
2349+
DomainFilter: endpoint.MatchAllDomainFilters{&domainFilter},
2350+
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
2351+
}
2352+
2353+
planned := plan.Calculate()
2354+
2355+
err = provider.ApplyChanges(context.Background(), planned.Changes)
2356+
if err != nil {
2357+
t.Errorf("should not fail - %s", err)
2358+
}
2359+
2360+
chs, chErr := provider.listCustomHostnamesWithPagination(ctx, "001")
2361+
if chErr != nil {
2362+
t.Errorf("should not fail - %s", chErr)
2363+
}
2364+
assert.Equal(t, len(chs), CustomHostnamesNumber)
2365+
}

0 commit comments

Comments
 (0)