fix(cloudflare): custom hostnames edge-cases causing duplicates (#5183)

* fix(cloudflare): custom hostnames edge-cases causing duplicates

* syntax/style

* Use %q log fmt for cloudflare provider code

* move custom hostnames related submitChanges() implementation to a separate method submitCustomHostnameChanges(); extend truncated logging

* use maps for DNS records getRecordID() and custom hostnames getCustomHostname() for faster lookups

* types for records/custom hostnames maps

* tidy up using underlying maps for dns records and custom hostnames

* style/naming

* fix private names

* combine unnecessarily separated conditions
This commit is contained in:
mrozentsvayg 2025-03-24 02:06:33 -07:00 committed by GitHub
parent f6d49ddbe8
commit 04f6a99c99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 285 additions and 125 deletions

View File

@ -55,6 +55,22 @@ var proxyEnabled *bool = boolPtr(true)
// proxyDisabled is a pointer to a bool false showing the record should not be proxied through cloudflare
var proxyDisabled *bool = boolPtr(false)
// for faster getRecordID() lookup
type DNSRecordIndex struct {
Name string
Type string
Content string
}
type DNSRecordsMap map[DNSRecordIndex]cloudflare.DNSRecord
// for faster getCustomHostname() lookup
type CustomHostnameIndex struct {
Hostname string
}
type CustomHostnamesMap map[CustomHostnameIndex]cloudflare.CustomHostname
var recordTypeProxyNotSupported = map[string]bool{
"LOC": true,
"MX": true,
@ -229,7 +245,7 @@ func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter prov
config, err = cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL"))
}
if err != nil {
return nil, fmt.Errorf("failed to initialize cloudflare provider: %v", err)
return nil, fmt.Errorf("failed to initialize cloudflare provider: %w", err)
}
provider := &CloudFlareProvider{
// Client: config,
@ -254,10 +270,10 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
if len(p.zoneIDFilter.ZoneIDs) > 0 && p.zoneIDFilter.ZoneIDs[0] != "" {
log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
log.Debugf("looking up zone %s", zoneID)
log.Debugf("looking up zone %q", zoneID)
detailResponse, err := p.Client.ZoneDetails(ctx, zoneID)
if err != nil {
log.Errorf("zone %s lookup failed, %v", zoneID, err)
log.Errorf("zone %q lookup failed, %v", zoneID, err)
return result, err
}
log.WithFields(log.Fields{
@ -285,7 +301,7 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
for _, zone := range zonesResponse.Result {
if !p.domainFilter.Match(zone.Name) {
log.Debugf("zone %s not in domain filter", zone.Name)
log.Debugf("zone %q not in domain filter", zone.Name)
continue
}
result = append(result, zone)
@ -359,6 +375,75 @@ func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Cha
return p.submitChanges(ctx, cloudflareChanges)
}
// submitCustomHostnameChanges implements Custom Hostname functionality for the Change, returns false if it fails
func (p *CloudFlareProvider) submitCustomHostnameChanges(ctx context.Context, zoneID string, change *cloudFlareChange, chs CustomHostnamesMap, logFields log.Fields) bool {
failedChange := false
// return early if disabled
if !p.CustomHostnamesConfig.Enabled {
return !failedChange
}
switch change.Action {
case cloudFlareUpdate:
if recordTypeCustomHostnameSupported[change.ResourceRecord.Type] {
prevChName := change.CustomHostnamePrev
newChName := change.CustomHostname.Hostname
if prevCh, err := getCustomHostname(chs, prevChName); err == nil {
prevChID := prevCh.ID
if prevChID != "" && prevChName != newChName {
log.WithFields(logFields).Infof("Removing previous custom hostname %q/%q", prevChID, prevChName)
chErr := p.Client.DeleteCustomHostname(ctx, zoneID, prevChID)
if chErr != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to remove previous custom hostname %q/%q: %v", prevChID, prevChName, chErr)
}
}
}
if newChName != "" && prevChName != newChName {
log.WithFields(logFields).Infof("Adding custom hostname %q", newChName)
_, chErr := p.Client.CreateCustomHostname(ctx, zoneID, change.CustomHostname)
if chErr != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to add custom hostname %q: %v", newChName, chErr)
}
}
}
case cloudFlareDelete:
if recordTypeCustomHostnameSupported[change.ResourceRecord.Type] && change.CustomHostname.Hostname != "" {
log.WithFields(logFields).Infof("Deleting custom hostname %q", change.CustomHostname.Hostname)
if ch, err := getCustomHostname(chs, change.CustomHostname.Hostname); err == nil {
chID := ch.ID
chErr := p.Client.DeleteCustomHostname(ctx, zoneID, chID)
if chErr != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to delete custom hostname %q/%q: %v", chID, change.CustomHostname.Hostname, chErr)
}
} else {
log.WithFields(logFields).Warnf("failed to delete custom hostname %q: %v", change.CustomHostname.Hostname, err)
}
}
case cloudFlareCreate:
if recordTypeCustomHostnameSupported[change.ResourceRecord.Type] && change.CustomHostname.Hostname != "" {
log.WithFields(logFields).Infof("Creating custom hostname %q", change.CustomHostname.Hostname)
if ch, err := getCustomHostname(chs, change.CustomHostname.Hostname); err == nil {
if change.CustomHostname.CustomOriginServer == ch.CustomOriginServer {
log.WithFields(logFields).Warnf("custom hostname %q already exists with the same origin %q, continue", change.CustomHostname.Hostname, ch.CustomOriginServer)
} else {
failedChange = true
log.WithFields(logFields).Errorf("failed to create custom hostname, %q already exists with origin %q", change.CustomHostname.Hostname, ch.CustomOriginServer)
}
} else {
_, chErr := p.Client.CreateCustomHostname(ctx, zoneID, change.CustomHostname)
if chErr != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to create custom hostname %q: %v", change.CustomHostname.Hostname, chErr)
}
}
}
}
return !failedChange
}
// submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloudFlareChange) error {
// return early if there is nothing to change
@ -395,37 +480,15 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
resourceContainer := cloudflare.ZoneIdentifier(zoneID)
records, err := p.listDNSRecordsWithAutoPagination(ctx, zoneID)
if err != nil {
return fmt.Errorf("could not fetch records from zone, %v", err)
return fmt.Errorf("could not fetch records from zone, %w", err)
}
chs, chErr := p.listCustomHostnamesWithPagination(ctx, zoneID)
if chErr != nil {
return fmt.Errorf("could not fetch custom hostnames from zone, %v", chErr)
}
if change.Action == cloudFlareUpdate {
if recordTypeCustomHostnameSupported[change.ResourceRecord.Type] {
prevCh := change.CustomHostnamePrev
newCh := change.CustomHostname.Hostname
if prevCh != "" {
prevChID, _ := p.getCustomHostnameOrigin(chs, prevCh)
if prevChID != "" && prevCh != newCh {
log.WithFields(logFields).Infof("Removing previous custom hostname %v/%v", prevChID, prevCh)
chErr := p.Client.DeleteCustomHostname(ctx, zoneID, prevChID)
if chErr != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to remove previous custom hostname %v/%v: %v", prevChID, prevCh, chErr)
}
}
}
if newCh != "" {
if prevCh != newCh {
log.WithFields(logFields).Infof("Adding custom hostname %v", newCh)
_, chErr := p.Client.CreateCustomHostname(ctx, zoneID, change.CustomHostname)
if chErr != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to add custom hostname %v: %v", newCh, chErr)
}
}
}
if !p.submitCustomHostnameChanges(ctx, zoneID, change, chs, logFields) {
failedChange = true
}
recordID := p.getRecordID(records, change.ResourceRecord)
if recordID == "" {
@ -457,19 +520,8 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
failedChange = true
log.WithFields(logFields).Errorf("failed to delete record: %v", err)
}
if change.CustomHostname.Hostname == "" {
continue
}
log.WithFields(logFields).Infof("Deleting custom hostname %v", change.CustomHostname.Hostname)
chID, _ := p.getCustomHostnameOrigin(chs, change.CustomHostname.Hostname)
if chID == "" {
log.WithFields(logFields).Infof("Custom hostname %v not found", change.CustomHostname.Hostname)
continue
}
chErr := p.Client.DeleteCustomHostname(ctx, zoneID, chID)
if chErr != nil {
if !p.submitCustomHostnameChanges(ctx, zoneID, change, chs, logFields) {
failedChange = true
log.WithFields(logFields).Errorf("failed to delete custom hostname %v/%v: %v", chID, change.CustomHostname.Hostname, chErr)
}
} else if change.Action == cloudFlareCreate {
recordParam := getCreateDNSRecordParam(*change)
@ -478,20 +530,8 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
failedChange = true
log.WithFields(logFields).Errorf("failed to create record: %v", err)
}
if change.CustomHostname.Hostname == "" {
continue
}
log.WithFields(logFields).Infof("Creating custom hostname %v", change.CustomHostname.Hostname)
chID, chOrigin := p.getCustomHostnameOrigin(chs, change.CustomHostname.Hostname)
if chID != "" {
if !p.submitCustomHostnameChanges(ctx, zoneID, change, chs, logFields) {
failedChange = true
log.WithFields(logFields).Errorf("failed to create custom hostname, %v already exists for origin %v", change.CustomHostname.Hostname, chOrigin)
continue
}
_, chErr := p.Client.CreateCustomHostname(ctx, zoneID, change.CustomHostname)
if chErr != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to create custom hostname %v: %v", change.CustomHostname.Hostname, chErr)
}
}
}
@ -501,7 +541,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
}
if len(failedZones) > 0 {
return fmt.Errorf("failed to submit all changes for the following zones: %v", failedZones)
return fmt.Errorf("failed to submit all changes for the following zones: %q", failedZones)
}
return nil
@ -535,7 +575,7 @@ func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []
for _, c := range changeSet {
zoneID, _ := zoneNameIDMapper.FindZone(c.ResourceRecord.Name)
if zoneID == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecord.Name)
log.Debugf("Skipping record %q because no hosted zone matching record DNS Name was detected", c.ResourceRecord.Name)
continue
}
changes[zoneID] = append(changes[zoneID], c)
@ -544,22 +584,21 @@ func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []
return changes
}
func (p *CloudFlareProvider) getRecordID(records []cloudflare.DNSRecord, record cloudflare.DNSRecord) string {
for _, zoneRecord := range records {
if zoneRecord.Name == record.Name && zoneRecord.Type == record.Type && zoneRecord.Content == record.Content {
return zoneRecord.ID
}
func (p *CloudFlareProvider) getRecordID(records DNSRecordsMap, record cloudflare.DNSRecord) string {
if zoneRecord, ok := records[DNSRecordIndex{Name: record.Name, Type: record.Type, Content: record.Content}]; ok {
return zoneRecord.ID
}
return ""
}
func (p *CloudFlareProvider) getCustomHostnameOrigin(chs []cloudflare.CustomHostname, hostname string) (string, string) {
for _, zoneCh := range chs {
if zoneCh.Hostname == hostname {
return zoneCh.ID, zoneCh.CustomOriginServer
}
func getCustomHostname(chs CustomHostnamesMap, chName string) (cloudflare.CustomHostname, error) {
if chName == "" {
return cloudflare.CustomHostname{}, fmt.Errorf("failed to get custom hostname: %q is empty", chName)
}
return "", ""
if ch, ok := chs[CustomHostnameIndex{Hostname: chName}]; ok {
return ch, nil
}
return cloudflare.CustomHostname{}, fmt.Errorf("failed to get custom hostname: %q not found", chName)
}
func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoint.Endpoint, target string, current *endpoint.Endpoint) *cloudFlareChange {
@ -580,7 +619,7 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi
newCustomHostname = cloudflare.CustomHostname{
Hostname: getEndpointCustomHostname(endpoint),
CustomOriginServer: endpoint.DNSName,
SSL: getCustomHostnamesSSLOptions(endpoint, p.CustomHostnamesConfig),
SSL: getCustomHostnamesSSLOptions(p.CustomHostnamesConfig),
}
}
return &cloudFlareChange{
@ -607,9 +646,14 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi
}
}
func newDNSRecordIndex(r cloudflare.DNSRecord) DNSRecordIndex {
return DNSRecordIndex{Name: r.Name, Type: r.Type, Content: r.Content}
}
// listDNSRecordsWithAutoPagination performs automatic pagination of results on requests to cloudflare.ListDNSRecords with custom per_page values
func (p *CloudFlareProvider) listDNSRecordsWithAutoPagination(ctx context.Context, zoneID string) ([]cloudflare.DNSRecord, error) {
var records []cloudflare.DNSRecord
func (p *CloudFlareProvider) listDNSRecordsWithAutoPagination(ctx context.Context, zoneID string) (DNSRecordsMap, error) {
// for faster getRecordID lookup
records := make(DNSRecordsMap)
resultInfo := cloudflare.ResultInfo{PerPage: p.DNSRecordsPerPage, Page: 1}
params := cloudflare.ListDNSRecordsParams{ResultInfo: resultInfo}
for {
@ -625,7 +669,9 @@ func (p *CloudFlareProvider) listDNSRecordsWithAutoPagination(ctx context.Contex
return nil, err
}
records = append(records, pageRecords...)
for _, r := range pageRecords {
records[newDNSRecordIndex(r)] = r
}
params.ResultInfo = resultInfo.Next()
if params.ResultInfo.Done() {
break
@ -634,12 +680,16 @@ func (p *CloudFlareProvider) listDNSRecordsWithAutoPagination(ctx context.Contex
return records, nil
}
func newCustomHostnameIndex(ch cloudflare.CustomHostname) CustomHostnameIndex {
return CustomHostnameIndex{Hostname: ch.Hostname}
}
// listCustomHostnamesWithPagination performs automatic pagination of results on requests to cloudflare.CustomHostnames
func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Context, zoneID string) ([]cloudflare.CustomHostname, error) {
func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Context, zoneID string) (CustomHostnamesMap, error) {
if !p.CustomHostnamesConfig.Enabled {
return nil, nil
}
var chs []cloudflare.CustomHostname
chs := make(CustomHostnamesMap)
resultInfo := cloudflare.ResultInfo{Page: 1}
for {
pageCustomHostnameListResponse, result, err := p.Client.CustomHostnames(ctx, zoneID, resultInfo.Page, cloudflare.CustomHostname{})
@ -651,11 +701,12 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte
return nil, provider.NewSoftError(err)
}
}
log.Errorf("zone %s failed to fetch custom hostnames. Please check if \"Cloudflare for SaaS\" is enabled and API key permissions, %v", zoneID, err)
log.Errorf("zone %q failed to fetch custom hostnames. Please check if \"Cloudflare for SaaS\" is enabled and API key permissions, %v", zoneID, err)
return nil, err
}
chs = append(chs, pageCustomHostnameListResponse...)
for _, ch := range pageCustomHostnameListResponse {
chs[newCustomHostnameIndex(ch)] = ch
}
resultInfo = result.Next()
if resultInfo.Done() {
break
@ -664,7 +715,7 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte
return chs, nil
}
func getCustomHostnamesSSLOptions(endpoint *endpoint.Endpoint, customHostnamesConfig CustomHostnamesConfig) *cloudflare.CustomHostnameSSL {
func getCustomHostnamesSSLOptions(customHostnamesConfig CustomHostnamesConfig) *cloudflare.CustomHostnameSSL {
return &cloudflare.CustomHostnameSSL{
Type: "dv",
Method: "http",
@ -683,7 +734,7 @@ func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool {
if v.Name == source.CloudflareProxiedKey {
b, err := strconv.ParseBool(v.Value)
if err != nil {
log.Errorf("Failed to parse annotation [%s]: %v", source.CloudflareProxiedKey, err)
log.Errorf("Failed to parse annotation [%q]: %v", source.CloudflareProxiedKey, err)
} else {
proxied = b
}
@ -706,7 +757,7 @@ func getEndpointCustomHostname(endpoint *endpoint.Endpoint) string {
return ""
}
func groupByNameAndTypeWithCustomHostnames(records []cloudflare.DNSRecord, chs []cloudflare.CustomHostname) []*endpoint.Endpoint {
func groupByNameAndTypeWithCustomHostnames(records DNSRecordsMap, chs CustomHostnamesMap) []*endpoint.Endpoint {
endpoints := []*endpoint.Endpoint{}
// group supported records by name and type

View File

@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"os"
"slices"
"sort"
"strings"
"testing"
@ -404,10 +405,10 @@ func getCustomHostnameIdxByID(chs []cloudflare.CustomHostname, customHostnameID
return -1
}
func (p *CloudFlareProvider) getCustomHostnameIDbyCustomHostnameAndOrigin(chs []cloudflare.CustomHostname, customHostname string, origin string) (string, string) {
for _, zoneCh := range chs {
if zoneCh.Hostname == customHostname && zoneCh.CustomOriginServer == origin {
return zoneCh.ID, zoneCh.Hostname
func getCustomHostnameIDbyCustomHostnameAndOrigin(chs CustomHostnamesMap, customHostname string, origin string) (string, string) {
for _, ch := range chs {
if ch.Hostname == customHostname && ch.CustomOriginServer == origin {
return ch.ID, ch.Hostname
}
}
@ -1029,19 +1030,19 @@ func TestCloudflareApplyChangesError(t *testing.T) {
func TestCloudflareGetRecordID(t *testing.T) {
p := &CloudFlareProvider{}
records := []cloudflare.DNSRecord{
{
recordsMap := DNSRecordsMap{
{Name: "foo.com", Type: endpoint.RecordTypeCNAME, Content: "foobar"}: {
Name: "foo.com",
Type: endpoint.RecordTypeCNAME,
Content: "foobar",
ID: "1",
},
{
{Name: "bar.de", Type: endpoint.RecordTypeA}: {
Name: "bar.de",
Type: endpoint.RecordTypeA,
ID: "2",
},
{
{Name: "bar.de", Type: endpoint.RecordTypeA, Content: "1.2.3.4"}: {
Name: "bar.de",
Type: endpoint.RecordTypeA,
Content: "1.2.3.4",
@ -1049,29 +1050,29 @@ func TestCloudflareGetRecordID(t *testing.T) {
},
}
assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{
assert.Equal(t, "", p.getRecordID(recordsMap, cloudflare.DNSRecord{
Name: "foo.com",
Type: endpoint.RecordTypeA,
Content: "foobar",
}))
assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{
assert.Equal(t, "", p.getRecordID(recordsMap, cloudflare.DNSRecord{
Name: "foo.com",
Type: endpoint.RecordTypeCNAME,
Content: "fizfuz",
}))
assert.Equal(t, "1", p.getRecordID(records, cloudflare.DNSRecord{
assert.Equal(t, "1", p.getRecordID(recordsMap, cloudflare.DNSRecord{
Name: "foo.com",
Type: endpoint.RecordTypeCNAME,
Content: "foobar",
}))
assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{
assert.Equal(t, "", p.getRecordID(recordsMap, cloudflare.DNSRecord{
Name: "bar.de",
Type: endpoint.RecordTypeA,
Content: "2.3.4.5",
}))
assert.Equal(t, "2", p.getRecordID(records, cloudflare.DNSRecord{
assert.Equal(t, "2", p.getRecordID(recordsMap, cloudflare.DNSRecord{
Name: "bar.de",
Type: endpoint.RecordTypeA,
Content: "1.2.3.4",
@ -1309,7 +1310,19 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
}
for _, tc := range testCases {
assert.ElementsMatch(t, groupByNameAndTypeWithCustomHostnames(tc.Records, []cloudflare.CustomHostname{}), tc.ExpectedEndpoints)
records := make(DNSRecordsMap)
for _, r := range tc.Records {
records[newDNSRecordIndex(r)] = r
}
endpoints := groupByNameAndTypeWithCustomHostnames(records, CustomHostnamesMap{})
// Targets order could be random with underlying map
for _, ep := range endpoints {
slices.Sort(ep.Targets)
}
for _, ep := range tc.ExpectedEndpoints {
slices.Sort(ep.Targets)
}
assert.ElementsMatch(t, endpoints, tc.ExpectedEndpoints)
}
}
@ -1866,9 +1879,9 @@ func TestCloudflareDNSRecordsOperationsFail(t *testing.T) {
err = provider.ApplyChanges(context.Background(), planned.Changes)
if err == nil && tc.shouldFail {
t.Errorf("should fail - %s, %s", tc.Name, err)
t.Errorf("should fail - %q, %v", tc.Name, err)
} else if err != nil && !tc.shouldFail {
t.Errorf("should not fail - %s, %s", tc.Name, err)
t.Errorf("should not fail - %q, %v", tc.Name, err)
}
}
}
@ -1908,10 +1921,10 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) {
shouldFail: true,
},
{
Name: "add custom hostname to more than one endpoint",
Name: "custom hostname to the same origin",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "fail.foo.bar.com",
DNSName: "origin.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4", "2.3.4.5"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
@ -1919,13 +1932,72 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) {
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "fail.foo.fancybar.com",
Value: "custom.foo.fancybar.com",
},
},
},
},
shouldFail: false,
},
{
Name: "same custom hostname to the another origin",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "another-origin.foo.bar.com",
Targets: endpoint.Targets{"3.4.5.6"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "custom.foo.fancybar.com",
},
},
},
},
shouldFail: true,
},
{
Name: "create CNAME records with custom hostname",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "c.foo.bar.com",
Targets: endpoint.Targets{"c.cname.foo.bar.com"},
RecordType: endpoint.RecordTypeCNAME,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "c.foo.fancybar.com",
},
},
},
},
shouldFail: false,
},
{
Name: "TXT registry record should not attempt to create custom hostname",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "cname-c.foo.bar.com",
Targets: endpoint.Targets{
"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/external-dns/my-domain-here-app",
},
RecordType: endpoint.RecordTypeTXT,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "c.foo.fancybar.com",
},
},
},
},
shouldFail: false,
},
{
Name: "failing to update custom hostname",
Endpoints: []*endpoint.Endpoint{
@ -2157,7 +2229,7 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) {
for _, tc := range testFailCases {
records, err := provider.Records(ctx)
if err != nil {
t.Errorf("should not fail, %s", err)
t.Errorf("should not fail, %v", err)
}
endpoints, err := provider.AdjustEndpoints(tc.Endpoints)
@ -2167,23 +2239,23 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) {
Current: records,
Desired: endpoints,
DomainFilter: endpoint.MatchAllDomainFilters{&domainFilter},
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeTXT},
}
planned := plan.Calculate()
err = provider.ApplyChanges(context.Background(), planned.Changes)
if err == nil && tc.shouldFail {
t.Errorf("should fail - %s, %s", tc.Name, err)
t.Errorf("should fail - %q, %v", tc.Name, err)
} else if err != nil && !tc.shouldFail {
t.Errorf("should not fail - %s, %s", tc.Name, err)
t.Errorf("should not fail - %q, %v", tc.Name, err)
}
}
for _, tc := range testCases {
records, err := provider.Records(ctx)
if err != nil {
t.Errorf("should not fail, %s", err)
t.Errorf("should not fail, %v", err)
}
endpoints, err := provider.AdjustEndpoints(tc.Endpoints)
@ -2200,16 +2272,16 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) {
err = provider.ApplyChanges(context.Background(), planned.Changes)
if err != nil {
t.Errorf("should not fail - %s, %s", tc.Name, err)
t.Errorf("should not fail - %q, %v", tc.Name, err)
}
chs, chErr := provider.listCustomHostnamesWithPagination(ctx, "001")
if chErr != nil {
t.Errorf("should not fail - %s, %s", tc.Name, chErr)
t.Errorf("should not fail - %q, %v", tc.Name, chErr)
}
for expectedOrigin, expectedCustomHostname := range tc.ExpectedCustomHostnames {
_, ch := provider.getCustomHostnameIDbyCustomHostnameAndOrigin(chs, expectedCustomHostname, expectedOrigin)
_, ch := getCustomHostnameIDbyCustomHostnameAndOrigin(chs, expectedCustomHostname, expectedOrigin)
assert.Equal(t, expectedCustomHostname, ch)
}
}
@ -2229,7 +2301,8 @@ func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) {
Name string
Endpoints []*endpoint.Endpoint
ExpectedCustomHostnames map[string]string
preApplyHook bool
preApplyHook string
logOutput string
}{
{
Name: "create DNS record with custom hostname",
@ -2248,20 +2321,49 @@ func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) {
},
},
},
preApplyHook: false,
preApplyHook: "",
logOutput: "",
},
{
Name: "remove DNS record with unexpectedly missing custom hostname",
Endpoints: []*endpoint.Endpoint{},
preApplyHook: true,
preApplyHook: "corrupt",
logOutput: "level=warning msg=\"failed to delete custom hostname \\\"newerror-getCustomHostnameOrigin.foo.fancybar.com\\\": failed to get custom hostname: \\\"newerror-getCustomHostnameOrigin.foo.fancybar.com\\\" not found\" action=DELETE record=create.foo.bar.com",
},
{
Name: "duplicate custom hostname",
Endpoints: []*endpoint.Endpoint{},
preApplyHook: "duplicate",
logOutput: "",
},
{
Name: "create DNS record with custom hostname",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "a.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "a.foo.fancybar.com",
},
},
},
},
preApplyHook: "",
logOutput: "custom hostname \\\"a.foo.fancybar.com\\\" already exists with the same origin \\\"a.foo.bar.com\\\", continue",
},
}
b := testutils.LogsToBuffer(log.InfoLevel, t)
for _, tc := range testCases {
b := testutils.LogsToBuffer(log.InfoLevel, t)
records, err := provider.Records(ctx)
if err != nil {
t.Errorf("should not fail, %s", err)
t.Errorf("should not fail, %v", err)
}
endpoints, err := provider.AdjustEndpoints(tc.Endpoints)
@ -2278,14 +2380,14 @@ func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) {
// manually corrupt custom hostname before the deletion step
// the purpose is to cause getCustomHostnameOrigin() to fail on change.Action == cloudFlareDelete
if tc.preApplyHook {
chs, chErr := provider.listCustomHostnamesWithPagination(ctx, zoneID)
if chErr != nil {
t.Errorf("should not fail - %s, %s", tc.Name, chErr)
}
chID, _ := provider.getCustomHostnameOrigin(chs, "newerror-getCustomHostnameOrigin.foo.fancybar.com")
if chID != "" {
t.Logf("corrupting custom hostname %v", chID)
chs, chErr := provider.listCustomHostnamesWithPagination(ctx, zoneID)
if chErr != nil {
t.Errorf("should not fail - %q, %v", tc.Name, chErr)
}
if tc.preApplyHook == "corrupt" {
if ch, err := getCustomHostname(chs, "newerror-getCustomHostnameOrigin.foo.fancybar.com"); err == nil {
chID := ch.ID
t.Logf("corrupting custom hostname %q", chID)
oldIdx := getCustomHostnameIdxByID(client.customHostnames[zoneID], chID)
oldCh := client.customHostnames[zoneID][oldIdx]
ch := cloudflare.CustomHostname{
@ -2295,14 +2397,21 @@ func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) {
}
client.customHostnames[zoneID][oldIdx] = ch
}
}
} else if tc.preApplyHook == "duplicate" { // manually inject duplicating custom hostname with the same name and origin
ch := cloudflare.CustomHostname{
ID: "ID-random-123",
Hostname: "a.foo.fancybar.com",
CustomOriginServer: "a.foo.bar.com",
}
client.customHostnames[zoneID] = append(client.customHostnames[zoneID], ch)
}
err = provider.ApplyChanges(context.Background(), planned.Changes)
if err != nil {
t.Errorf("should not fail - %s, %s", tc.Name, err)
t.Errorf("should not fail - %q, %v", tc.Name, err)
}
assert.Contains(t, b.String(), tc.logOutput)
}
assert.Contains(t, b.String(), "level=info msg=\"Custom hostname newerror-getCustomHostnameOrigin.foo.fancybar.com not found\" action=DELETE record=create.foo.bar.com")
}
func TestCloudflareListCustomHostnamesWithPagionation(t *testing.T) {
@ -2337,7 +2446,7 @@ func TestCloudflareListCustomHostnamesWithPagionation(t *testing.T) {
records, err := provider.Records(ctx)
if err != nil {
t.Errorf("should not fail, %s", err)
t.Errorf("should not fail, %v", err)
}
endpoints, err := provider.AdjustEndpoints(generatedEndpoints)
@ -2354,12 +2463,12 @@ func TestCloudflareListCustomHostnamesWithPagionation(t *testing.T) {
err = provider.ApplyChanges(context.Background(), planned.Changes)
if err != nil {
t.Errorf("should not fail - %s", err)
t.Errorf("should not fail - %v", err)
}
chs, chErr := provider.listCustomHostnamesWithPagination(ctx, "001")
if chErr != nil {
t.Errorf("should not fail - %s", chErr)
t.Errorf("should not fail - %v", chErr)
}
assert.Equal(t, len(chs), CustomHostnamesNumber)
}