mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
Merge pull request #5175 from vflaux/fix_cf_regional_hostname
fix(cloudflare): regional hostnames
This commit is contained in:
commit
2de3b50b14
@ -27,7 +27,6 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cloudflare "github.com/cloudflare/cloudflare-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@ -74,6 +73,11 @@ type CustomHostnameIndex struct {
|
||||
|
||||
type CustomHostnamesMap map[CustomHostnameIndex]cloudflare.CustomHostname
|
||||
|
||||
type DataLocalizationRegionalHostnameChange struct {
|
||||
Action string
|
||||
cloudflare.RegionalHostname
|
||||
}
|
||||
|
||||
var recordTypeProxyNotSupported = map[string]bool{
|
||||
"LOC": true,
|
||||
"MX": true,
|
||||
@ -94,6 +98,12 @@ var recordTypeCustomHostnameSupported = map[string]bool{
|
||||
"CNAME": true,
|
||||
}
|
||||
|
||||
var recordTypeRegionalHostnameSupported = map[string]bool{
|
||||
"A": true,
|
||||
"AAAA": true,
|
||||
"CNAME": true,
|
||||
}
|
||||
|
||||
// cloudFlareDNS is the subset of the CloudFlare API that we actually use. Add methods as required. Signatures must match exactly.
|
||||
type cloudFlareDNS interface {
|
||||
UserDetails(ctx context.Context) (cloudflare.User, error)
|
||||
@ -105,7 +115,9 @@ 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
|
||||
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
|
||||
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)
|
||||
@ -140,11 +152,20 @@ func (z zoneService) UpdateDNSRecord(ctx context.Context, rc *cloudflare.Resourc
|
||||
return err
|
||||
}
|
||||
|
||||
func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error {
|
||||
_, err := z.service.CreateDataLocalizationRegionalHostname(ctx, rc, rp)
|
||||
return err
|
||||
}
|
||||
|
||||
func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error {
|
||||
_, err := z.service.UpdateDataLocalizationRegionalHostname(ctx, rc, rp)
|
||||
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) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error {
|
||||
return z.service.DeleteDNSRecord(ctx, rc, recordID)
|
||||
}
|
||||
@ -208,11 +229,19 @@ func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams
|
||||
}
|
||||
}
|
||||
|
||||
// createDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
|
||||
func createDataLocalizationRegionalHostnameParams(rhc DataLocalizationRegionalHostnameChange) cloudflare.CreateDataLocalizationRegionalHostnameParams {
|
||||
return cloudflare.CreateDataLocalizationRegionalHostnameParams{
|
||||
Hostname: rhc.Hostname,
|
||||
RegionKey: rhc.RegionKey,
|
||||
}
|
||||
}
|
||||
|
||||
// updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
|
||||
func updateDataLocalizationRegionalHostnameParams(cfc cloudFlareChange) cloudflare.UpdateDataLocalizationRegionalHostnameParams {
|
||||
func updateDataLocalizationRegionalHostnameParams(rhc DataLocalizationRegionalHostnameChange) cloudflare.UpdateDataLocalizationRegionalHostnameParams {
|
||||
return cloudflare.UpdateDataLocalizationRegionalHostnameParams{
|
||||
Hostname: cfc.RegionalHostname.Hostname,
|
||||
RegionKey: cfc.RegionalHostname.RegionKey,
|
||||
Hostname: rhc.Hostname,
|
||||
RegionKey: rhc.RegionKey,
|
||||
}
|
||||
}
|
||||
|
||||
@ -466,6 +495,129 @@ func (p *CloudFlareProvider) submitCustomHostnameChanges(ctx context.Context, zo
|
||||
return !failedChange
|
||||
}
|
||||
|
||||
// submitDataLocalizationRegionalHostnameChanges applies a set of data localization regional hostname changes, returns false if it fails
|
||||
func (p *CloudFlareProvider) submitDataLocalizationRegionalHostnameChanges(ctx context.Context, changes []DataLocalizationRegionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool {
|
||||
failedChange := false
|
||||
|
||||
for _, change := range changes {
|
||||
logFields := log.Fields{
|
||||
"hostname": change.Hostname,
|
||||
"region_key": change.RegionKey,
|
||||
"action": change.Action,
|
||||
"zone": resourceContainer.Identifier,
|
||||
}
|
||||
log.WithFields(logFields).Info("Changing regional hostname")
|
||||
switch change.Action {
|
||||
case cloudFlareCreate:
|
||||
log.WithFields(logFields).Debug("Creating regional hostname")
|
||||
if p.DryRun {
|
||||
continue
|
||||
}
|
||||
regionalHostnameParam := createDataLocalizationRegionalHostnameParams(change)
|
||||
err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam)
|
||||
if err != nil {
|
||||
var apiErr *cloudflare.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusConflict {
|
||||
log.WithFields(logFields).Debug("Regional hostname already exists, updating instead")
|
||||
params := updateDataLocalizationRegionalHostnameParams(change)
|
||||
err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, params)
|
||||
if err != nil {
|
||||
failedChange = true
|
||||
log.WithFields(logFields).Errorf("failed to update regional hostname: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
failedChange = true
|
||||
log.WithFields(logFields).Errorf("failed to create regional hostname: %v", err)
|
||||
}
|
||||
case cloudFlareUpdate:
|
||||
log.WithFields(logFields).Debug("Updating regional hostname")
|
||||
if p.DryRun {
|
||||
continue
|
||||
}
|
||||
regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(change)
|
||||
err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam)
|
||||
if err != nil {
|
||||
var apiErr *cloudflare.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
||||
log.WithFields(logFields).Debug("Regional hostname not does not exists, creating instead")
|
||||
params := createDataLocalizationRegionalHostnameParams(change)
|
||||
err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, params)
|
||||
if err != nil {
|
||||
failedChange = true
|
||||
log.WithFields(logFields).Errorf("failed to create regional hostname: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
failedChange = true
|
||||
log.WithFields(logFields).Errorf("failed to update regional hostname: %v", err)
|
||||
}
|
||||
case cloudFlareDelete:
|
||||
log.WithFields(logFields).Debug("Deleting regional hostname")
|
||||
if p.DryRun {
|
||||
continue
|
||||
}
|
||||
err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, resourceContainer, change.Hostname)
|
||||
if err != nil {
|
||||
var apiErr *cloudflare.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
||||
log.WithFields(logFields).Debug("Regional hostname does not exists, nothing to do")
|
||||
continue
|
||||
}
|
||||
failedChange = true
|
||||
log.WithFields(logFields).Errorf("failed to delete regional hostname: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !failedChange
|
||||
}
|
||||
|
||||
// dataLocalizationRegionalHostnamesChanges processes a slice of cloudFlare changes and consolidates them
|
||||
// into a list of data localization regional hostname changes.
|
||||
// returns nil if no changes are needed
|
||||
func dataLocalizationRegionalHostnamesChanges(changes []*cloudFlareChange) ([]DataLocalizationRegionalHostnameChange, error) {
|
||||
regionalHostnameChanges := make(map[string]DataLocalizationRegionalHostnameChange)
|
||||
for _, change := range changes {
|
||||
if change.RegionalHostname.Hostname == "" {
|
||||
continue
|
||||
}
|
||||
if change.RegionalHostname.RegionKey == "" {
|
||||
return nil, fmt.Errorf("region key is empty for regional hostname %q", change.RegionalHostname.Hostname)
|
||||
}
|
||||
regionalHostname, ok := regionalHostnameChanges[change.RegionalHostname.Hostname]
|
||||
switch change.Action {
|
||||
case cloudFlareCreate, cloudFlareUpdate:
|
||||
if !ok {
|
||||
regionalHostnameChanges[change.RegionalHostname.Hostname] = DataLocalizationRegionalHostnameChange{
|
||||
Action: change.Action,
|
||||
RegionalHostname: change.RegionalHostname,
|
||||
}
|
||||
continue
|
||||
}
|
||||
if regionalHostname.RegionKey != change.RegionalHostname.RegionKey {
|
||||
return nil, fmt.Errorf("conflicting region keys for regional hostname %q: %q and %q", change.RegionalHostname.Hostname, regionalHostname.RegionKey, change.RegionalHostname.RegionKey)
|
||||
}
|
||||
if (change.Action == cloudFlareUpdate && regionalHostname.Action != cloudFlareUpdate) ||
|
||||
regionalHostname.Action == cloudFlareDelete {
|
||||
regionalHostnameChanges[change.RegionalHostname.Hostname] = DataLocalizationRegionalHostnameChange{
|
||||
Action: cloudFlareUpdate,
|
||||
RegionalHostname: change.RegionalHostname,
|
||||
}
|
||||
}
|
||||
case cloudFlareDelete:
|
||||
if !ok {
|
||||
regionalHostnameChanges[change.RegionalHostname.Hostname] = DataLocalizationRegionalHostnameChange{
|
||||
Action: cloudFlareDelete,
|
||||
RegionalHostname: change.RegionalHostname,
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return slices.Collect(maps.Values(regionalHostnameChanges)), nil
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -484,6 +636,8 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
|
||||
var failedZones []string
|
||||
for zoneID, zoneChanges := range changesByZone {
|
||||
var failedChange bool
|
||||
resourceContainer := cloudflare.ZoneIdentifier(zoneID)
|
||||
|
||||
for _, change := range zoneChanges {
|
||||
logFields := log.Fields{
|
||||
"record": change.ResourceRecord.Name,
|
||||
@ -499,7 +653,6 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
|
||||
continue
|
||||
}
|
||||
|
||||
resourceContainer := cloudflare.ZoneIdentifier(zoneID)
|
||||
records, err := p.listDNSRecordsWithAutoPagination(ctx, zoneID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not fetch records from zone, %w", err)
|
||||
@ -524,13 +677,6 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
|
||||
failedChange = true
|
||||
log.WithFields(logFields).Errorf("failed to update record: %v", err)
|
||||
}
|
||||
if regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(*change); regionalHostnameParam.RegionKey != "" {
|
||||
regionalHostnameErr := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam)
|
||||
if regionalHostnameErr != nil {
|
||||
failedChange = true
|
||||
log.WithFields(logFields).Errorf("failed to update record when editing region: %v", regionalHostnameErr)
|
||||
}
|
||||
}
|
||||
} else if change.Action == cloudFlareDelete {
|
||||
recordID := p.getRecordID(records, change.ResourceRecord)
|
||||
if recordID == "" {
|
||||
@ -557,6 +703,19 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if regionalHostnamesChanges, err := dataLocalizationRegionalHostnamesChanges(zoneChanges); err == nil {
|
||||
if !p.submitDataLocalizationRegionalHostnameChanges(ctx, regionalHostnamesChanges, resourceContainer) {
|
||||
failedChange = true
|
||||
}
|
||||
} else {
|
||||
logFields := log.Fields{
|
||||
"zone": zoneID,
|
||||
}
|
||||
log.WithFields(logFields).Errorf("failed to build data localization regional hostname changes: %v", err)
|
||||
failedChange = true
|
||||
}
|
||||
|
||||
if failedChange {
|
||||
failedZones = append(failedZones, zoneID)
|
||||
}
|
||||
@ -649,7 +808,6 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
|
||||
if ep.RecordTTL.IsConfigured() {
|
||||
ttl = int(ep.RecordTTL)
|
||||
}
|
||||
dt := time.Now()
|
||||
|
||||
prevCustomHostnames := []string{}
|
||||
newCustomHostnames := map[string]cloudflare.CustomHostname{}
|
||||
@ -661,6 +819,13 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
|
||||
newCustomHostnames[v] = p.newCustomHostname(v, ep.DNSName)
|
||||
}
|
||||
}
|
||||
regionalHostname := cloudflare.RegionalHostname{}
|
||||
if regionKey := getRegionKey(ep, p.RegionKey); regionKey != "" {
|
||||
regionalHostname = cloudflare.RegionalHostname{
|
||||
Hostname: ep.DNSName,
|
||||
RegionKey: regionKey,
|
||||
}
|
||||
}
|
||||
return &cloudFlareChange{
|
||||
Action: action,
|
||||
ResourceRecord: cloudflare.DNSRecord{
|
||||
@ -671,15 +836,8 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
|
||||
Proxied: &proxied,
|
||||
Type: ep.RecordType,
|
||||
Content: target,
|
||||
Meta: map[string]interface{}{
|
||||
"region": p.RegionKey,
|
||||
},
|
||||
},
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: ep.DNSName,
|
||||
RegionKey: p.RegionKey,
|
||||
CreatedOn: &dt,
|
||||
},
|
||||
RegionalHostname: regionalHostname,
|
||||
CustomHostnamesPrev: prevCustomHostnames,
|
||||
CustomHostnames: newCustomHostnames,
|
||||
}
|
||||
@ -787,6 +945,19 @@ func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool {
|
||||
return proxied
|
||||
}
|
||||
|
||||
func getRegionKey(endpoint *endpoint.Endpoint, defaultRegionKey string) string {
|
||||
if !recordTypeRegionalHostnameSupported[endpoint.RecordType] {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, v := range endpoint.ProviderSpecific {
|
||||
if v.Name == source.CloudflareRegionKey {
|
||||
return v.Value
|
||||
}
|
||||
}
|
||||
return defaultRegionKey
|
||||
}
|
||||
|
||||
func getEndpointCustomHostnames(ep *endpoint.Endpoint) []string {
|
||||
for _, v := range ep.ProviderSpecific {
|
||||
if v.Name == source.CloudflareCustomHostnameKey {
|
||||
|
@ -21,16 +21,16 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
cloudflare "github.com/cloudflare/cloudflare-go"
|
||||
"github.com/maxatome/go-testdeep/td"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/maxatome/go-testdeep/td"
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/internal/testutils"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
@ -38,10 +38,11 @@ import (
|
||||
)
|
||||
|
||||
type MockAction struct {
|
||||
Name string
|
||||
ZoneId string
|
||||
RecordId string
|
||||
RecordData cloudflare.DNSRecord
|
||||
Name string
|
||||
ZoneId string
|
||||
RecordId string
|
||||
RecordData cloudflare.DNSRecord
|
||||
RegionalHostname cloudflare.RegionalHostname
|
||||
}
|
||||
|
||||
type mockCloudFlareClient struct {
|
||||
@ -225,6 +226,19 @@ func (m *mockCloudFlareClient) UpdateDNSRecord(ctx context.Context, rc *cloudfla
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCloudFlareClient) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "CreateDataLocalizationRegionalHostname",
|
||||
ZoneId: rc.Identifier,
|
||||
RecordId: "",
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: rp.Hostname,
|
||||
RegionKey: rp.RegionKey,
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCloudFlareClient) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "UpdateDataLocalizationRegionalHostname",
|
||||
@ -237,6 +251,18 @@ func (m *mockCloudFlareClient) UpdateDataLocalizationRegionalHostname(ctx contex
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCloudFlareClient) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "DeleteDataLocalizationRegionalHostname",
|
||||
ZoneId: rc.Identifier,
|
||||
RecordId: "",
|
||||
RecordData: cloudflare.DNSRecord{
|
||||
Name: hostname,
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "Delete",
|
||||
@ -745,6 +771,110 @@ func TestCloudflareSetProxied(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudflareRegionalHostname(t *testing.T) {
|
||||
endpoints := []*endpoint.Endpoint{
|
||||
{
|
||||
RecordType: "A",
|
||||
DNSName: "bar.com",
|
||||
Targets: endpoint.Targets{"127.0.0.1", "127.0.0.2"},
|
||||
ProviderSpecific: endpoint.ProviderSpecific{
|
||||
{
|
||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key",
|
||||
Value: "eu",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
AssertActions(t, &CloudFlareProvider{RegionKey: "us"}, endpoints, []MockAction{
|
||||
{
|
||||
Name: "Create",
|
||||
ZoneId: "001",
|
||||
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
|
||||
RecordData: cloudflare.DNSRecord{
|
||||
ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
|
||||
Type: "A",
|
||||
Name: "bar.com",
|
||||
Content: "127.0.0.1",
|
||||
TTL: 1,
|
||||
Proxied: proxyDisabled,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Create",
|
||||
ZoneId: "001",
|
||||
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
|
||||
RecordData: cloudflare.DNSRecord{
|
||||
ID: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
|
||||
Type: "A",
|
||||
Name: "bar.com",
|
||||
Content: "127.0.0.2",
|
||||
TTL: 1,
|
||||
Proxied: proxyDisabled,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "CreateDataLocalizationRegionalHostname",
|
||||
ZoneId: "001",
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "bar.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
},
|
||||
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCloudflareRegionalHostnameDefaults(t *testing.T) {
|
||||
endpoints := []*endpoint.Endpoint{
|
||||
{
|
||||
RecordType: "A",
|
||||
DNSName: "bar.com",
|
||||
Targets: endpoint.Targets{"127.0.0.1", "127.0.0.2"},
|
||||
},
|
||||
}
|
||||
|
||||
AssertActions(t, &CloudFlareProvider{RegionKey: "us"}, endpoints, []MockAction{
|
||||
{
|
||||
Name: "Create",
|
||||
ZoneId: "001",
|
||||
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
|
||||
RecordData: cloudflare.DNSRecord{
|
||||
ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
|
||||
Type: "A",
|
||||
Name: "bar.com",
|
||||
Content: "127.0.0.1",
|
||||
TTL: 1,
|
||||
Proxied: proxyDisabled,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Create",
|
||||
ZoneId: "001",
|
||||
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
|
||||
RecordData: cloudflare.DNSRecord{
|
||||
ID: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
|
||||
Type: "A",
|
||||
Name: "bar.com",
|
||||
Content: "127.0.0.2",
|
||||
TTL: 1,
|
||||
Proxied: proxyDisabled,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "CreateDataLocalizationRegionalHostname",
|
||||
ZoneId: "001",
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "bar.com",
|
||||
RegionKey: "us",
|
||||
},
|
||||
},
|
||||
},
|
||||
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCloudflareZones(t *testing.T) {
|
||||
provider := &CloudFlareProvider{
|
||||
Client: NewMockCloudFlareClient(),
|
||||
@ -1575,24 +1705,6 @@ func TestCloudFlareProvider_Region(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudFlareProvider_updateDataLocalizationRegionalHostnameParams(t *testing.T) {
|
||||
change := &cloudFlareChange{
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example.com",
|
||||
RegionKey: "us",
|
||||
},
|
||||
}
|
||||
|
||||
params := updateDataLocalizationRegionalHostnameParams(*change)
|
||||
if params.Hostname != "example.com" {
|
||||
t.Errorf("expected hostname to be 'example.com', but got '%s'", params.Hostname)
|
||||
}
|
||||
|
||||
if params.RegionKey != "us" {
|
||||
t.Errorf("expected region key to be 'us', but got '%s'", params.RegionKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
|
||||
_ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx")
|
||||
_ = os.Setenv("CF_API_EMAIL", "test@test.com")
|
||||
@ -1609,8 +1721,9 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
|
||||
}
|
||||
|
||||
endpoint := &endpoint.Endpoint{
|
||||
DNSName: "example.com",
|
||||
Targets: []string{"192.0.2.1"},
|
||||
DNSName: "example.com",
|
||||
RecordType: "A",
|
||||
Targets: []string{"192.0.2.1"},
|
||||
}
|
||||
|
||||
change := provider.newCloudFlareChange(cloudFlareCreate, endpoint, endpoint.Targets[0], nil)
|
||||
@ -2679,3 +2792,334 @@ func TestCloudflareListCustomHostnamesWithPagionation(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, len(chs), CustomHostnamesNumber)
|
||||
}
|
||||
|
||||
func Test_getRegionKey(t *testing.T) {
|
||||
type args struct {
|
||||
endpoint *endpoint.Endpoint
|
||||
defaultRegionKey string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no region key",
|
||||
args: args{
|
||||
endpoint: &endpoint.Endpoint{
|
||||
RecordType: "A",
|
||||
},
|
||||
defaultRegionKey: "",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "default region key",
|
||||
args: args{
|
||||
endpoint: &endpoint.Endpoint{
|
||||
RecordType: "A",
|
||||
},
|
||||
defaultRegionKey: "us",
|
||||
},
|
||||
want: "us",
|
||||
},
|
||||
{
|
||||
name: "endpoint with region key",
|
||||
args: args{
|
||||
endpoint: &endpoint.Endpoint{
|
||||
RecordType: "A",
|
||||
ProviderSpecific: endpoint.ProviderSpecific{
|
||||
{
|
||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key",
|
||||
Value: "eu",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultRegionKey: "us",
|
||||
},
|
||||
want: "eu",
|
||||
},
|
||||
{
|
||||
name: "endpoint with empty region key",
|
||||
args: args{
|
||||
endpoint: &endpoint.Endpoint{
|
||||
RecordType: "A",
|
||||
ProviderSpecific: endpoint.ProviderSpecific{
|
||||
{
|
||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultRegionKey: "us",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "unsupported record type",
|
||||
args: args{
|
||||
endpoint: &endpoint.Endpoint{
|
||||
RecordType: "TXT",
|
||||
ProviderSpecific: endpoint.ProviderSpecific{
|
||||
{
|
||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key",
|
||||
Value: "eu",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultRegionKey: "us",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getRegionKey(tt.args.endpoint, tt.args.defaultRegionKey); got != tt.want {
|
||||
t.Errorf("getRegionKey() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_dataLocalizationRegionalHostnamesChanges(t *testing.T) {
|
||||
cmpDataLocalizationRegionalHostnameChange := func(i, j DataLocalizationRegionalHostnameChange) int {
|
||||
if i.Action == j.Action {
|
||||
return 0
|
||||
}
|
||||
if i.Hostname < j.Hostname {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
type args struct {
|
||||
changes []*cloudFlareChange
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []DataLocalizationRegionalHostnameChange
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
args: args{
|
||||
changes: []*cloudFlareChange{},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "changes without RegionalHostname",
|
||||
args: args{
|
||||
changes: []*cloudFlareChange{
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
ResourceRecord: cloudflare.DNSRecord{
|
||||
Name: "example.com",
|
||||
},
|
||||
RegionalHostname: cloudflare.RegionalHostname{}, // Empty
|
||||
},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "change with empty RegionKey",
|
||||
args: args{
|
||||
changes: []*cloudFlareChange{
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
ResourceRecord: cloudflare.DNSRecord{
|
||||
Name: "example.com",
|
||||
},
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example.com",
|
||||
RegionKey: "", // Empty region key
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "conflicting region keys",
|
||||
args: args{
|
||||
changes: []*cloudFlareChange{
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example.com",
|
||||
RegionKey: "us", // Different region key for same hostname
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "update takes precedence over create & delete",
|
||||
args: args{
|
||||
changes: []*cloudFlareChange{
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareUpdate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareDelete,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []DataLocalizationRegionalHostnameChange{
|
||||
{
|
||||
Action: cloudFlareUpdate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "create after delete becomes update",
|
||||
args: args{
|
||||
changes: []*cloudFlareChange{
|
||||
{
|
||||
Action: cloudFlareDelete,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []DataLocalizationRegionalHostnameChange{
|
||||
{
|
||||
Action: cloudFlareUpdate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "consolidate mixed actions for different hostnames",
|
||||
args: args{
|
||||
changes: []*cloudFlareChange{
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example1.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareUpdate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example2.com",
|
||||
RegionKey: "us",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareDelete,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example3.com",
|
||||
RegionKey: "ap",
|
||||
},
|
||||
},
|
||||
// duplicated actions
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example1.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareUpdate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example2.com",
|
||||
RegionKey: "us",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareDelete,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example3.com",
|
||||
RegionKey: "ap",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []DataLocalizationRegionalHostnameChange{
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example1.com",
|
||||
RegionKey: "eu",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareUpdate,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example2.com",
|
||||
RegionKey: "us",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareDelete,
|
||||
RegionalHostname: cloudflare.RegionalHostname{
|
||||
Hostname: "example3.com",
|
||||
RegionKey: "ap",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := dataLocalizationRegionalHostnamesChanges(tt.args.changes)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("dataLocalizationRegionalHostnamesChanges() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
slices.SortFunc(got, cmpDataLocalizationRegionalHostnameChange)
|
||||
slices.SortFunc(tt.want, cmpDataLocalizationRegionalHostnameChange)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("dataLocalizationRegionalHostnamesChanges() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,7 @@ const (
|
||||
// The annotation used for determining if traffic will go through Cloudflare
|
||||
CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied"
|
||||
CloudflareCustomHostnameKey = "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname"
|
||||
CloudflareRegionKey = "external-dns.alpha.kubernetes.io/cloudflare-region-key"
|
||||
|
||||
SetIdentifierKey = "external-dns.alpha.kubernetes.io/set-identifier"
|
||||
)
|
||||
@ -202,6 +203,12 @@ func getProviderSpecificAnnotations(annotations map[string]string) (endpoint.Pro
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
if v, exists := annotations[CloudflareRegionKey]; exists {
|
||||
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
|
||||
Name: CloudflareRegionKey,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
if getAliasFromAnnotations(annotations) {
|
||||
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
|
||||
Name: "alias",
|
||||
|
@ -174,6 +174,41 @@ func TestGetProviderSpecificCloudflareAnnotations(t *testing.T) {
|
||||
t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %s", tc.expectedKey, tc.expectedValue)
|
||||
})
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
title string
|
||||
annotations map[string]string
|
||||
expectedKey string
|
||||
expectedValue string
|
||||
}{
|
||||
{
|
||||
title: "Cloudflare region key annotation is set correctly",
|
||||
annotations: map[string]string{CloudflareRegionKey: "us"},
|
||||
expectedKey: CloudflareRegionKey,
|
||||
expectedValue: "us",
|
||||
},
|
||||
{
|
||||
title: "Cloudflare region key annotation among another annotations is set correctly",
|
||||
annotations: map[string]string{
|
||||
"random annotation 1": "random value 1",
|
||||
CloudflareRegionKey: "us",
|
||||
"random annotation 2": "random value 2",
|
||||
},
|
||||
expectedKey: CloudflareRegionKey,
|
||||
expectedValue: "us",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.title, func(t *testing.T) {
|
||||
providerSpecificAnnotations, _ := getProviderSpecificAnnotations(tc.annotations)
|
||||
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
|
||||
if providerSpecificAnnotation.Name == tc.expectedKey {
|
||||
assert.Equal(t, tc.expectedValue, providerSpecificAnnotation.Value)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %v", tc.expectedKey, tc.expectedValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProviderSpecificAliasAnnotations(t *testing.T) {
|
||||
|
Loading…
Reference in New Issue
Block a user