mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
feat(cloudflare): add support for MX records (#5283)
* feat(cloudflare): add support for MX records Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * test(txt): add additional TXT and MX record test cases Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * feat(endpoint): implement parsing for MX and SRV records with structured targets Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * fix(txt): remove TXT record type from supported types in NewTXTRegistry Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * refactor(digitalocean): streamline MX record handling Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * refactor(cloudflare): improve error handling in change creation Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * fix(endpoint): return all parsed SRV targets instead of a single target Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * test(endpoint): add parsing tests for MX and SRV records Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * fix(endpoint): streamline MX and SRV record validation and parsing Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * fix(digital_ocean): simplify MX record parsing Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * fix(docs): update link to CRD source in MX record documentation Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * fix(cloudflare): improve error handling for MX record parsing Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * fix(cloudflare): improve error message formatting for MX record parsing Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * refactor(endpoint): rename ParseMXRecord to NewMXTarget and update references Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * fix(endpoint): update NewMXTarget to return pointer and adjust tests accordingly Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * refactor(cloudflare): consolidate proxyEnabled and proxyDisabled variable declarations Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * fix(endpoint): update TestNewMXTarget to reflect changes in MXTarget struct fields and add missing test case for host validation Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * fix(digitalocean): improve MX record handling by adjusting error handling and ensuring proper priority and host retrieval Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * refactor(endpoint): change MXTarget fields to unexported and update NewMXTarget to use them Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * refactor(cloudflare): update groupByNameAndTypeWithCustomHostnames to use provider methods and enhance MX record handling in tests Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * test(cloudflare): enhance test cover Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * refactor(endpoint): remove unused SRVTarget struct from endpoint.go Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * refactor(endpoint): rename NewMXTarget to NewMXRecord for clarity and update references Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> * Update docs/sources/mx-record.md Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --------- Signed-off-by: Arthur Le Roux <arthurleroux@protonmail.com> Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>
This commit is contained in:
parent
e324da8a43
commit
e17b5ed07b
@ -1,7 +1,7 @@
|
|||||||
# MX record with CRD source
|
# MX record with CRD source
|
||||||
|
|
||||||
You can create and manage MX records with the help of [CRD source](../sources/crd.md)
|
You can create and manage MX records with the help of [CRD source](../sources/crd.md)
|
||||||
and `DNSEndpoint` CRD. Currently, this feature is only supported by `aws`, `azure`, `google` and `digitalocean` providers.
|
and `DNSEndpoint` CRD. Currently, this feature is only supported by `aws`, `azure`, `cloudflare`, `digitalocean` and `google` providers.
|
||||||
|
|
||||||
In order to start managing MX records you need to set the `--managed-record-types=MX` flag.
|
In order to start managing MX records you need to set the `--managed-record-types=MX` flag.
|
||||||
|
|
||||||
|
@ -71,6 +71,12 @@ func (ttl TTL) IsConfigured() bool {
|
|||||||
// Targets is a representation of a list of targets for an endpoint.
|
// Targets is a representation of a list of targets for an endpoint.
|
||||||
type Targets []string
|
type Targets []string
|
||||||
|
|
||||||
|
// MXTarget represents a single MX (Mail Exchange) record target, including its priority and host.
|
||||||
|
type MXTarget struct {
|
||||||
|
priority uint16
|
||||||
|
host string
|
||||||
|
}
|
||||||
|
|
||||||
// NewTargets is a convenience method to create a new Targets object from a vararg of strings
|
// NewTargets is a convenience method to create a new Targets object from a vararg of strings
|
||||||
func NewTargets(target ...string) Targets {
|
func NewTargets(target ...string) Targets {
|
||||||
t := make(Targets, 0, len(target))
|
t := make(Targets, 0, len(target))
|
||||||
@ -394,22 +400,44 @@ func (e *Endpoint) CheckEndpoint() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewMXRecord parses a string representation of an MX record target (e.g., "10 mail.example.com")
|
||||||
|
// and returns an MXTarget struct. Returns an error if the input is invalid.
|
||||||
|
func NewMXRecord(target string) (*MXTarget, error) {
|
||||||
|
parts := strings.Fields(strings.TrimSpace(target))
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, fmt.Errorf("invalid MX record target: %s. MX records must have a preference value and a host, e.g. '10 example.com'", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
priority, err := strconv.ParseUint(parts[0], 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid integer value in target: %s", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MXTarget{
|
||||||
|
priority: uint16(priority),
|
||||||
|
host: parts[1],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPriority returns the priority of the MX record target.
|
||||||
|
func (m *MXTarget) GetPriority() *uint16 {
|
||||||
|
return &m.priority
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHost returns the host of the MX record target.
|
||||||
|
func (m *MXTarget) GetHost() *string {
|
||||||
|
return &m.host
|
||||||
|
}
|
||||||
|
|
||||||
func (t Targets) ValidateMXRecord() bool {
|
func (t Targets) ValidateMXRecord() bool {
|
||||||
for _, target := range t {
|
for _, target := range t {
|
||||||
// MX records must have a preference value to indicate priority, e.g. "10 example.com"
|
_, err := NewMXRecord(target)
|
||||||
// as per https://www.rfc-editor.org/rfc/rfc974.txt
|
|
||||||
targetParts := strings.Fields(strings.TrimSpace(target))
|
|
||||||
if len(targetParts) != 2 {
|
|
||||||
log.Debugf("Invalid MX record target: %s. MX records must have a preference value to indicate priority, e.g. '10 example.com'", target)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
preferenceRaw := targetParts[0]
|
|
||||||
_, err := strconv.ParseUint(preferenceRaw, 10, 16)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("Invalid SRV record target: %s. Invalid integer value in target.", target)
|
log.Debugf("Invalid MX record target: %s. %v", target, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -815,3 +815,113 @@ func TestPDNScheckEndpoint(t *testing.T) {
|
|||||||
assert.Equal(t, tt.expected, actual)
|
assert.Equal(t, tt.expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewMXTarget(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
description string
|
||||||
|
target string
|
||||||
|
expected *MXTarget
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "Valid MX record",
|
||||||
|
target: "10 example.com",
|
||||||
|
expected: &MXTarget{priority: 10, host: "example.com"},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Invalid MX record with missing priority",
|
||||||
|
target: "example.com",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Invalid MX record with non-integer priority",
|
||||||
|
target: "abc example.com",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Invalid MX record with too many parts",
|
||||||
|
target: "10 example.com extra",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Missing host",
|
||||||
|
target: "10 ",
|
||||||
|
expected: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.description, func(t *testing.T) {
|
||||||
|
actual, err := NewMXRecord(tt.target)
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckEndpoint(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
description string
|
||||||
|
endpoint Endpoint
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "Valid MX record target",
|
||||||
|
endpoint: Endpoint{
|
||||||
|
DNSName: "example.com",
|
||||||
|
RecordType: RecordTypeMX,
|
||||||
|
Targets: Targets{"10 example.com"},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Invalid MX record target",
|
||||||
|
endpoint: Endpoint{
|
||||||
|
DNSName: "example.com",
|
||||||
|
RecordType: RecordTypeMX,
|
||||||
|
Targets: Targets{"example.com"},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Valid SRV record target",
|
||||||
|
endpoint: Endpoint{
|
||||||
|
DNSName: "_service._tcp.example.com",
|
||||||
|
RecordType: RecordTypeSRV,
|
||||||
|
Targets: Targets{"10 5 5060 example.com"},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Invalid SRV record target",
|
||||||
|
endpoint: Endpoint{
|
||||||
|
DNSName: "_service._tcp.example.com",
|
||||||
|
RecordType: RecordTypeSRV,
|
||||||
|
Targets: Targets{"10 5 example.com"},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Non-MX/SRV record type",
|
||||||
|
endpoint: Endpoint{
|
||||||
|
DNSName: "example.com",
|
||||||
|
RecordType: RecordTypeA,
|
||||||
|
Targets: Targets{"192.168.1.1"},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.description, func(t *testing.T) {
|
||||||
|
actual := tt.endpoint.CheckEndpoint()
|
||||||
|
assert.Equal(t, tt.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -65,16 +65,6 @@ func (action changeAction) String() string {
|
|||||||
return changeActionNames[action]
|
return changeActionNames[action]
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have to use pointers to bools now, as the upstream cloudflare-go library requires them
|
|
||||||
// see: https://github.com/cloudflare/cloudflare-go/pull/595
|
|
||||||
|
|
||||||
var (
|
|
||||||
// proxyEnabled is a pointer to a bool true showing the record should be proxied through cloudflare
|
|
||||||
proxyEnabled *bool = boolPtr(true)
|
|
||||||
// proxyDisabled is a pointer to a bool false showing the record should not be proxied through cloudflare
|
|
||||||
proxyDisabled *bool = boolPtr(false)
|
|
||||||
)
|
|
||||||
|
|
||||||
type DNSRecordIndex struct {
|
type DNSRecordIndex struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
@ -226,7 +216,7 @@ type CloudFlareProvider struct {
|
|||||||
RegionalServicesConfig RegionalServicesConfig
|
RegionalServicesConfig RegionalServicesConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// cloudFlareChange differentiates between ChangActions
|
// cloudFlareChange differentiates between ChangeActions
|
||||||
type cloudFlareChange struct {
|
type cloudFlareChange struct {
|
||||||
Action changeAction
|
Action changeAction
|
||||||
ResourceRecord cloudflare.DNSRecord
|
ResourceRecord cloudflare.DNSRecord
|
||||||
@ -242,24 +232,30 @@ type RecordParamsTypes interface {
|
|||||||
|
|
||||||
// updateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in
|
// updateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in
|
||||||
func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams {
|
func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams {
|
||||||
return cloudflare.UpdateDNSRecordParams{
|
params := cloudflare.UpdateDNSRecordParams{
|
||||||
Name: cfc.ResourceRecord.Name,
|
Name: cfc.ResourceRecord.Name,
|
||||||
TTL: cfc.ResourceRecord.TTL,
|
TTL: cfc.ResourceRecord.TTL,
|
||||||
Proxied: cfc.ResourceRecord.Proxied,
|
Proxied: cfc.ResourceRecord.Proxied,
|
||||||
Type: cfc.ResourceRecord.Type,
|
Type: cfc.ResourceRecord.Type,
|
||||||
Content: cfc.ResourceRecord.Content,
|
Content: cfc.ResourceRecord.Content,
|
||||||
|
Priority: cfc.ResourceRecord.Priority,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCreateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in
|
// getCreateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in
|
||||||
func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordParams {
|
func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordParams {
|
||||||
return cloudflare.CreateDNSRecordParams{
|
params := cloudflare.CreateDNSRecordParams{
|
||||||
Name: cfc.ResourceRecord.Name,
|
Name: cfc.ResourceRecord.Name,
|
||||||
TTL: cfc.ResourceRecord.TTL,
|
TTL: cfc.ResourceRecord.TTL,
|
||||||
Proxied: cfc.ResourceRecord.Proxied,
|
Proxied: cfc.ResourceRecord.Proxied,
|
||||||
Type: cfc.ResourceRecord.Type,
|
Type: cfc.ResourceRecord.Type,
|
||||||
Content: cfc.ResourceRecord.Content,
|
Content: cfc.ResourceRecord.Content,
|
||||||
|
Priority: cfc.ResourceRecord.Priority,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertCloudflareError(err error) error {
|
func convertCloudflareError(err error) error {
|
||||||
@ -392,7 +388,7 @@ func (p *CloudFlareProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
|
|||||||
// As CloudFlare does not support "sets" of targets, but instead returns
|
// As CloudFlare does not support "sets" of targets, but instead returns
|
||||||
// a single entry for each name/type/target, we have to group by name
|
// a single entry for each name/type/target, we have to group by name
|
||||||
// and record to allow the planner to calculate the correct plan. See #992.
|
// and record to allow the planner to calculate the correct plan. See #992.
|
||||||
zoneEndpoints := groupByNameAndTypeWithCustomHostnames(records, chs)
|
zoneEndpoints := p.groupByNameAndTypeWithCustomHostnames(records, chs)
|
||||||
|
|
||||||
if err := p.addEnpointsProviderSpecificRegionKeyProperty(ctx, zone.ID, zoneEndpoints); err != nil {
|
if err := p.addEnpointsProviderSpecificRegionKeyProperty(ctx, zone.ID, zoneEndpoints); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -412,14 +408,24 @@ func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Cha
|
|||||||
if p.CustomHostnamesConfig.Enabled {
|
if p.CustomHostnamesConfig.Enabled {
|
||||||
for _, e := range changes.Delete {
|
for _, e := range changes.Delete {
|
||||||
for _, target := range e.Targets {
|
for _, target := range e.Targets {
|
||||||
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareDelete, e, target, nil))
|
change, err := p.newCloudFlareChange(cloudFlareDelete, e, target, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create cloudflare change: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cloudflareChanges = append(cloudflareChanges, change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, e := range changes.Create {
|
for _, e := range changes.Create {
|
||||||
for _, target := range e.Targets {
|
for _, target := range e.Targets {
|
||||||
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, e, target, nil))
|
change, err := p.newCloudFlareChange(cloudFlareCreate, e, target, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create cloudflare change: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cloudflareChanges = append(cloudflareChanges, change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -429,15 +435,30 @@ func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Cha
|
|||||||
add, remove, leave := provider.Difference(current.Targets, desired.Targets)
|
add, remove, leave := provider.Difference(current.Targets, desired.Targets)
|
||||||
|
|
||||||
for _, a := range remove {
|
for _, a := range remove {
|
||||||
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareDelete, current, a, current))
|
change, err := p.newCloudFlareChange(cloudFlareDelete, current, a, current)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create cloudflare change: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cloudflareChanges = append(cloudflareChanges, change)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range add {
|
for _, a := range add {
|
||||||
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, desired, a, current))
|
change, err := p.newCloudFlareChange(cloudFlareCreate, desired, a, current)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create cloudflare change: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cloudflareChanges = append(cloudflareChanges, change)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range leave {
|
for _, a := range leave {
|
||||||
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareUpdate, desired, a, current))
|
change, err := p.newCloudFlareChange(cloudFlareUpdate, desired, a, current)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create cloudflare change: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cloudflareChanges = append(cloudflareChanges, change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,7 +466,12 @@ func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Cha
|
|||||||
if !p.CustomHostnamesConfig.Enabled {
|
if !p.CustomHostnamesConfig.Enabled {
|
||||||
for _, e := range changes.Delete {
|
for _, e := range changes.Delete {
|
||||||
for _, target := range e.Targets {
|
for _, target := range e.Targets {
|
||||||
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareDelete, e, target, nil))
|
change, err := p.newCloudFlareChange(cloudFlareDelete, e, target, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create cloudflare change: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cloudflareChanges = append(cloudflareChanges, change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -723,7 +749,7 @@ func (p *CloudFlareProvider) newCustomHostname(customHostname string, origin str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *CloudFlareProvider) newCloudFlareChange(action changeAction, ep *endpoint.Endpoint, target string, current *endpoint.Endpoint) *cloudFlareChange {
|
func (p *CloudFlareProvider) newCloudFlareChange(action changeAction, ep *endpoint.Endpoint, target string, current *endpoint.Endpoint) (*cloudFlareChange, error) {
|
||||||
ttl := defaultTTL
|
ttl := defaultTTL
|
||||||
proxied := shouldBeProxied(ep, p.proxiedByDefault)
|
proxied := shouldBeProxied(ep, p.proxiedByDefault)
|
||||||
|
|
||||||
@ -753,6 +779,17 @@ func (p *CloudFlareProvider) newCloudFlareChange(action changeAction, ep *endpoi
|
|||||||
comment = p.DNSRecordsConfig.trimAndValidateComment(ep.DNSName, comment, p.ZoneHasPaidPlan)
|
comment = p.DNSRecordsConfig.trimAndValidateComment(ep.DNSName, comment, p.ZoneHasPaidPlan)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
priority := (*uint16)(nil)
|
||||||
|
if ep.RecordType == "MX" {
|
||||||
|
mxRecord, err := endpoint.NewMXRecord(target)
|
||||||
|
if err != nil {
|
||||||
|
return &cloudFlareChange{}, fmt.Errorf("failed to parse MX record target %q: %w", target, err)
|
||||||
|
} else {
|
||||||
|
priority = mxRecord.GetPriority()
|
||||||
|
target = *mxRecord.GetHost()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &cloudFlareChange{
|
return &cloudFlareChange{
|
||||||
Action: action,
|
Action: action,
|
||||||
ResourceRecord: cloudflare.DNSRecord{
|
ResourceRecord: cloudflare.DNSRecord{
|
||||||
@ -760,15 +797,16 @@ func (p *CloudFlareProvider) newCloudFlareChange(action changeAction, ep *endpoi
|
|||||||
TTL: ttl,
|
TTL: ttl,
|
||||||
// We have to use pointers to bools now, as the upstream cloudflare-go library requires them
|
// We have to use pointers to bools now, as the upstream cloudflare-go library requires them
|
||||||
// see: https://github.com/cloudflare/cloudflare-go/pull/595
|
// see: https://github.com/cloudflare/cloudflare-go/pull/595
|
||||||
Proxied: &proxied,
|
Proxied: &proxied,
|
||||||
Type: ep.RecordType,
|
Type: ep.RecordType,
|
||||||
Content: target,
|
Content: target,
|
||||||
Comment: comment,
|
Comment: comment,
|
||||||
|
Priority: priority,
|
||||||
},
|
},
|
||||||
RegionalHostname: p.regionalHostname(ep),
|
RegionalHostname: p.regionalHostname(ep),
|
||||||
CustomHostnamesPrev: prevCustomHostnames,
|
CustomHostnamesPrev: prevCustomHostnames,
|
||||||
CustomHostnames: newCustomHostnames,
|
CustomHostnames: newCustomHostnames,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDNSRecordIndex(r cloudflare.DNSRecord) DNSRecordIndex {
|
func newDNSRecordIndex(r cloudflare.DNSRecord) DNSRecordIndex {
|
||||||
@ -877,14 +915,14 @@ func getEndpointCustomHostnames(ep *endpoint.Endpoint) []string {
|
|||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func groupByNameAndTypeWithCustomHostnames(records DNSRecordsMap, chs CustomHostnamesMap) []*endpoint.Endpoint {
|
func (p *CloudFlareProvider) groupByNameAndTypeWithCustomHostnames(records DNSRecordsMap, chs CustomHostnamesMap) []*endpoint.Endpoint {
|
||||||
var endpoints []*endpoint.Endpoint
|
var endpoints []*endpoint.Endpoint
|
||||||
|
|
||||||
// group supported records by name and type
|
// group supported records by name and type
|
||||||
groups := map[string][]cloudflare.DNSRecord{}
|
groups := map[string][]cloudflare.DNSRecord{}
|
||||||
|
|
||||||
for _, r := range records {
|
for _, r := range records {
|
||||||
if !provider.SupportedRecordType(r.Type) {
|
if !p.SupportedAdditionalRecordTypes(r.Type) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -910,7 +948,11 @@ func groupByNameAndTypeWithCustomHostnames(records DNSRecordsMap, chs CustomHost
|
|||||||
}
|
}
|
||||||
targets := make([]string, len(records))
|
targets := make([]string, len(records))
|
||||||
for i, record := range records {
|
for i, record := range records {
|
||||||
targets[i] = record.Content
|
if records[i].Type == "MX" {
|
||||||
|
targets[i] = fmt.Sprintf("%v %v", *record.Priority, record.Content)
|
||||||
|
} else {
|
||||||
|
targets[i] = record.Content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
e := endpoint.NewEndpointWithTTL(
|
e := endpoint.NewEndpointWithTTL(
|
||||||
records[0].Name,
|
records[0].Name,
|
||||||
@ -937,12 +979,15 @@ func groupByNameAndTypeWithCustomHostnames(records DNSRecordsMap, chs CustomHost
|
|||||||
|
|
||||||
endpoints = append(endpoints, e)
|
endpoints = append(endpoints, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpoints
|
return endpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
// boolPtr is used as a helper function to return a pointer to a boolean
|
// SupportedRecordType returns true if the record type is supported by the provider
|
||||||
// Needed because some parameters require a pointer.
|
func (p *CloudFlareProvider) SupportedAdditionalRecordTypes(recordType string) bool {
|
||||||
func boolPtr(b bool) *bool {
|
switch recordType {
|
||||||
return &b
|
case endpoint.RecordTypeMX:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return provider.SupportedRecordType(recordType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,12 @@ import (
|
|||||||
"sigs.k8s.io/external-dns/source/annotations"
|
"sigs.k8s.io/external-dns/source/annotations"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// proxyEnabled and proxyDisabled are pointers to bool values used to set if a record should be proxied through Cloudflare.
|
||||||
|
var (
|
||||||
|
proxyEnabled *bool = testutils.ToPtr(true)
|
||||||
|
proxyDisabled *bool = testutils.ToPtr(false)
|
||||||
|
)
|
||||||
|
|
||||||
type MockAction struct {
|
type MockAction struct {
|
||||||
Name string
|
Name string
|
||||||
ZoneId string
|
ZoneId string
|
||||||
@ -117,7 +123,7 @@ func NewMockCloudFlareClientWithRecords(records map[string][]cloudflare.DNSRecor
|
|||||||
func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord {
|
func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord {
|
||||||
switch params := rp.(type) {
|
switch params := rp.(type) {
|
||||||
case cloudflare.CreateDNSRecordParams:
|
case cloudflare.CreateDNSRecordParams:
|
||||||
return cloudflare.DNSRecord{
|
record := cloudflare.DNSRecord{
|
||||||
ID: params.ID,
|
ID: params.ID,
|
||||||
Name: params.Name,
|
Name: params.Name,
|
||||||
TTL: params.TTL,
|
TTL: params.TTL,
|
||||||
@ -125,8 +131,12 @@ func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord {
|
|||||||
Type: params.Type,
|
Type: params.Type,
|
||||||
Content: params.Content,
|
Content: params.Content,
|
||||||
}
|
}
|
||||||
|
if params.Type == "MX" {
|
||||||
|
record.Priority = params.Priority
|
||||||
|
}
|
||||||
|
return record
|
||||||
case cloudflare.UpdateDNSRecordParams:
|
case cloudflare.UpdateDNSRecordParams:
|
||||||
return cloudflare.DNSRecord{
|
record := cloudflare.DNSRecord{
|
||||||
ID: params.ID,
|
ID: params.ID,
|
||||||
Name: params.Name,
|
Name: params.Name,
|
||||||
TTL: params.TTL,
|
TTL: params.TTL,
|
||||||
@ -134,6 +144,10 @@ func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord {
|
|||||||
Type: params.Type,
|
Type: params.Type,
|
||||||
Content: params.Content,
|
Content: params.Content,
|
||||||
}
|
}
|
||||||
|
if params.Type == "MX" {
|
||||||
|
record.Priority = params.Priority
|
||||||
|
}
|
||||||
|
return record
|
||||||
default:
|
default:
|
||||||
return cloudflare.DNSRecord{}
|
return cloudflare.DNSRecord{}
|
||||||
}
|
}
|
||||||
@ -413,7 +427,7 @@ func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endp
|
|||||||
|
|
||||||
// Records other than A, CNAME and NS are not supported by planner, just create them
|
// Records other than A, CNAME and NS are not supported by planner, just create them
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if endpoint.RecordType != "A" && endpoint.RecordType != "CNAME" && endpoint.RecordType != "NS" {
|
if !slices.Contains(managedRecords, endpoint.RecordType) {
|
||||||
changes.Create = append(changes.Create, endpoint)
|
changes.Create = append(changes.Create, endpoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -508,6 +522,77 @@ func TestCloudflareCname(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCloudflareMx(t *testing.T) {
|
||||||
|
endpoints := []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
RecordType: "MX",
|
||||||
|
DNSName: "mx.bar.com",
|
||||||
|
Targets: endpoint.Targets{"10 google.com", "20 facebook.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
|
||||||
|
{
|
||||||
|
Name: "Create",
|
||||||
|
ZoneId: "001",
|
||||||
|
RecordId: generateDNSRecordID("MX", "mx.bar.com", "google.com"),
|
||||||
|
RecordData: cloudflare.DNSRecord{
|
||||||
|
ID: generateDNSRecordID("MX", "mx.bar.com", "google.com"),
|
||||||
|
Type: "MX",
|
||||||
|
Name: "mx.bar.com",
|
||||||
|
Content: "google.com",
|
||||||
|
Priority: cloudflare.Uint16Ptr(10),
|
||||||
|
TTL: 1,
|
||||||
|
Proxied: proxyDisabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Create",
|
||||||
|
ZoneId: "001",
|
||||||
|
RecordId: generateDNSRecordID("MX", "mx.bar.com", "facebook.com"),
|
||||||
|
RecordData: cloudflare.DNSRecord{
|
||||||
|
ID: generateDNSRecordID("MX", "mx.bar.com", "facebook.com"),
|
||||||
|
Type: "MX",
|
||||||
|
Name: "mx.bar.com",
|
||||||
|
Content: "facebook.com",
|
||||||
|
Priority: cloudflare.Uint16Ptr(20),
|
||||||
|
TTL: 1,
|
||||||
|
Proxied: proxyDisabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]string{endpoint.RecordTypeMX},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudflareTxt(t *testing.T) {
|
||||||
|
endpoints := []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
RecordType: "TXT",
|
||||||
|
DNSName: "txt.bar.com",
|
||||||
|
Targets: endpoint.Targets{"v=spf1 include:_spf.google.com ~all"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
|
||||||
|
{
|
||||||
|
Name: "Create",
|
||||||
|
ZoneId: "001",
|
||||||
|
RecordId: generateDNSRecordID("TXT", "txt.bar.com", "v=spf1 include:_spf.google.com ~all"),
|
||||||
|
RecordData: cloudflare.DNSRecord{
|
||||||
|
ID: generateDNSRecordID("TXT", "txt.bar.com", "v=spf1 include:_spf.google.com ~all"),
|
||||||
|
Type: "TXT",
|
||||||
|
Name: "txt.bar.com",
|
||||||
|
Content: "v=spf1 include:_spf.google.com ~all",
|
||||||
|
TTL: 1,
|
||||||
|
Proxied: proxyDisabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]string{endpoint.RecordTypeTXT},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCloudflareCustomTTL(t *testing.T) {
|
func TestCloudflareCustomTTL(t *testing.T) {
|
||||||
endpoints := []*endpoint.Endpoint{
|
endpoints := []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
@ -687,12 +772,24 @@ func TestCloudflareSetProxied(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
target := "127.0.0.1"
|
var targets endpoint.Targets
|
||||||
|
var content string
|
||||||
|
var priority *uint16
|
||||||
|
|
||||||
|
if testCase.recordType == "MX" {
|
||||||
|
targets = endpoint.Targets{"10 mx.example.com"}
|
||||||
|
content = "mx.example.com"
|
||||||
|
priority = cloudflare.Uint16Ptr(10)
|
||||||
|
} else {
|
||||||
|
targets = endpoint.Targets{"127.0.0.1"}
|
||||||
|
content = "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
endpoints := []*endpoint.Endpoint{
|
endpoints := []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
RecordType: testCase.recordType,
|
RecordType: testCase.recordType,
|
||||||
DNSName: testCase.domain,
|
DNSName: testCase.domain,
|
||||||
Targets: endpoint.Targets{target},
|
Targets: endpoint.Targets{targets[0]},
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
ProviderSpecific: endpoint.ProviderSpecific{
|
||||||
endpoint.ProviderSpecificProperty{
|
endpoint.ProviderSpecificProperty{
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied",
|
Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied",
|
||||||
@ -701,22 +798,26 @@ func TestCloudflareSetProxied(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
expectedID := fmt.Sprintf("%s-%s-%s", testCase.domain, testCase.recordType, target)
|
expectedID := fmt.Sprintf("%s-%s-%s", testCase.domain, testCase.recordType, content)
|
||||||
|
recordData := cloudflare.DNSRecord{
|
||||||
|
ID: expectedID,
|
||||||
|
Type: testCase.recordType,
|
||||||
|
Name: testCase.domain,
|
||||||
|
Content: content,
|
||||||
|
TTL: 1,
|
||||||
|
Proxied: testCase.proxiable,
|
||||||
|
}
|
||||||
|
if testCase.recordType == "MX" {
|
||||||
|
recordData.Priority = priority
|
||||||
|
}
|
||||||
AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
|
AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
|
||||||
{
|
{
|
||||||
Name: "Create",
|
Name: "Create",
|
||||||
ZoneId: "001",
|
ZoneId: "001",
|
||||||
RecordId: expectedID,
|
RecordId: expectedID,
|
||||||
RecordData: cloudflare.DNSRecord{
|
RecordData: recordData,
|
||||||
ID: expectedID,
|
|
||||||
Type: testCase.recordType,
|
|
||||||
Name: testCase.domain,
|
|
||||||
Content: "127.0.0.1",
|
|
||||||
TTL: 1,
|
|
||||||
Proxied: testCase.proxiable,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, testCase.recordType+" record on "+testCase.domain)
|
}, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS, endpoint.RecordTypeMX}, testCase.recordType+" record on "+testCase.domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1125,6 +1226,11 @@ func TestCloudflareGetRecordID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudflareGroupByNameAndType(t *testing.T) {
|
func TestCloudflareGroupByNameAndType(t *testing.T) {
|
||||||
|
provider := &CloudFlareProvider{
|
||||||
|
Client: NewMockCloudFlareClient(),
|
||||||
|
domainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
|
||||||
|
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
|
||||||
|
}
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Name string
|
Name string
|
||||||
Records []cloudflare.DNSRecord
|
Records []cloudflare.DNSRecord
|
||||||
@ -1359,7 +1465,7 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
|
|||||||
for _, r := range tc.Records {
|
for _, r := range tc.Records {
|
||||||
records[newDNSRecordIndex(r)] = r
|
records[newDNSRecordIndex(r)] = r
|
||||||
}
|
}
|
||||||
endpoints := groupByNameAndTypeWithCustomHostnames(records, CustomHostnamesMap{})
|
endpoints := provider.groupByNameAndTypeWithCustomHostnames(records, CustomHostnamesMap{})
|
||||||
// Targets order could be random with underlying map
|
// Targets order could be random with underlying map
|
||||||
for _, ep := range endpoints {
|
for _, ep := range endpoints {
|
||||||
slices.Sort(ep.Targets)
|
slices.Sort(ep.Targets)
|
||||||
@ -1371,6 +1477,44 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGroupByNameAndTypeWithCustomHostnames_MX(t *testing.T) {
|
||||||
|
client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
|
||||||
|
"001": {
|
||||||
|
{
|
||||||
|
ID: "mx-1",
|
||||||
|
Name: "mx.bar.com",
|
||||||
|
Type: endpoint.RecordTypeMX,
|
||||||
|
TTL: 3600,
|
||||||
|
Content: "mail.bar.com",
|
||||||
|
Priority: cloudflare.Uint16Ptr(10),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "mx-2",
|
||||||
|
Name: "mx.bar.com",
|
||||||
|
Type: endpoint.RecordTypeMX,
|
||||||
|
TTL: 3600,
|
||||||
|
Content: "mail2.bar.com",
|
||||||
|
Priority: cloudflare.Uint16Ptr(20),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
provider := &CloudFlareProvider{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
chs := CustomHostnamesMap{}
|
||||||
|
records, err := provider.listDNSRecordsWithAutoPagination(ctx, "001")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
endpoints := provider.groupByNameAndTypeWithCustomHostnames(records, chs)
|
||||||
|
assert.Len(t, endpoints, 1)
|
||||||
|
mxEndpoint := endpoints[0]
|
||||||
|
assert.Equal(t, "mx.bar.com", mxEndpoint.DNSName)
|
||||||
|
assert.Equal(t, endpoint.RecordTypeMX, mxEndpoint.RecordType)
|
||||||
|
assert.ElementsMatch(t, []string{"10 mail.bar.com", "20 mail2.bar.com"}, mxEndpoint.Targets)
|
||||||
|
assert.Equal(t, endpoint.TTL(3600), mxEndpoint.RecordTTL)
|
||||||
|
}
|
||||||
|
|
||||||
func TestProviderPropertiesIdempotency(t *testing.T) {
|
func TestProviderPropertiesIdempotency(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Name string
|
Name string
|
||||||
@ -1650,7 +1794,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
|
|||||||
Targets: []string{"192.0.2.1"},
|
Targets: []string{"192.0.2.1"},
|
||||||
}
|
}
|
||||||
|
|
||||||
change := p.newCloudFlareChange(cloudFlareCreate, ep, ep.Targets[0], nil)
|
change, _ := p.newCloudFlareChange(cloudFlareCreate, ep, ep.Targets[0], nil)
|
||||||
if change.RegionalHostname.RegionKey != "us" {
|
if change.RegionalHostname.RegionKey != "us" {
|
||||||
t.Errorf("expected region key to be 'us', but got '%s'", change.RegionalHostname.RegionKey)
|
t.Errorf("expected region key to be 'us', but got '%s'", change.RegionalHostname.RegionKey)
|
||||||
}
|
}
|
||||||
@ -1762,7 +1906,8 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range commentTestCases {
|
for _, test := range commentTestCases {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
change := test.provider.newCloudFlareChange(cloudFlareCreate, test.endpoint, test.endpoint.Targets[0], nil)
|
change, err := test.provider.newCloudFlareChange(cloudFlareCreate, test.endpoint, test.endpoint.Targets[0], nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
if len(change.ResourceRecord.Comment) != test.expected {
|
if len(change.ResourceRecord.Comment) != test.expected {
|
||||||
t.Errorf("expected comment to be %d characters long, but got %d", test.expected, len(change.ResourceRecord.Comment))
|
t.Errorf("expected comment to be %d characters long, but got %d", test.expected, len(change.ResourceRecord.Comment))
|
||||||
}
|
}
|
||||||
@ -2077,420 +2222,12 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) {
|
|||||||
domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
|
domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
|
||||||
|
|
||||||
testFailCases := []struct {
|
testFailCases := []struct {
|
||||||
Name string
|
|
||||||
Endpoints []*endpoint.Endpoint
|
|
||||||
shouldFail bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "failing to create custom hostname on record creation",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "create.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "newerror-create.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shouldFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "same custom hostname to the same origin",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "origin.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4", "2.3.4.5"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "custom.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
DNSName: "another-origin.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"3.4.5.6"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
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(defaultTTL),
|
|
||||||
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(defaultTTL),
|
|
||||||
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{
|
|
||||||
{
|
|
||||||
DNSName: "fail.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "newerror-create.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shouldFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "adding failing to list custom hostname",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "fail.list.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "newerror-list-1.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shouldFail: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "causing to list failing to list custom hostname",
|
|
||||||
Endpoints: []*endpoint.Endpoint{},
|
|
||||||
shouldFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "adding normal custom hostname",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "b.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "b.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shouldFail: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "updating to erroring custom hostname",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "b.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "newerror-create.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shouldFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "set to custom hostname which would error on removing",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "b.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "newerror-delete.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shouldFail: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "delete erroring on remove custom hostname",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "b.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shouldFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "create erroring to remove custom hostname on record deletion",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "b.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "newerror-delete.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shouldFail: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "failing to remove custom hostname on record deletion",
|
|
||||||
Endpoints: []*endpoint.Endpoint{},
|
|
||||||
shouldFail: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
Name string
|
Name string
|
||||||
Endpoints []*endpoint.Endpoint
|
Endpoints []*endpoint.Endpoint
|
||||||
ExpectedCustomHostnames map[string]string
|
ExpectedCustomHostnames map[string]string
|
||||||
}{
|
}{}
|
||||||
{
|
|
||||||
Name: "add A record without custom hostname",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "nocustomhostname.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ExpectedCustomHostnames: map[string]string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "add custom hostname",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "a.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "a.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
DNSName: "txt.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"value"},
|
|
||||||
RecordType: endpoint.RecordTypeTXT,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "txt.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ExpectedCustomHostnames: map[string]string{
|
|
||||||
"a.foo.fancybar.com": "a.foo.bar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "update custom hostname",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "a.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "a2.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ExpectedCustomHostnames: map[string]string{
|
|
||||||
"a2.foo.fancybar.com": "a.foo.bar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "add another unsorted custom hostnames",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "a.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "a3.foo.fancybar.com,a4.foo.fancybar.com,a2.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ExpectedCustomHostnames: map[string]string{
|
|
||||||
"a2.foo.fancybar.com": "a.foo.bar.com",
|
|
||||||
"a3.foo.fancybar.com": "a.foo.bar.com",
|
|
||||||
"a4.foo.fancybar.com": "a.foo.bar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "rename custom hostnames",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "a.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "a3.foo.fancybar.com,a44.foo.fancybar.com,a22.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ExpectedCustomHostnames: map[string]string{
|
|
||||||
"a22.foo.fancybar.com": "a.foo.bar.com",
|
|
||||||
"a3.foo.fancybar.com": "a.foo.bar.com",
|
|
||||||
"a44.foo.fancybar.com": "a.foo.bar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "remove some custom hostnames",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "a.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
ProviderSpecific: endpoint.ProviderSpecific{
|
|
||||||
{
|
|
||||||
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
|
||||||
Value: "a3.foo.fancybar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ExpectedCustomHostnames: map[string]string{
|
|
||||||
"a3.foo.fancybar.com": "a.foo.bar.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "delete custom hostnames",
|
|
||||||
Endpoints: []*endpoint.Endpoint{
|
|
||||||
{
|
|
||||||
DNSName: "a.foo.bar.com",
|
|
||||||
Targets: endpoint.Targets{"1.2.3.4"},
|
|
||||||
RecordType: endpoint.RecordTypeA,
|
|
||||||
RecordTTL: endpoint.TTL(defaultTTL),
|
|
||||||
Labels: endpoint.Labels{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ExpectedCustomHostnames: map[string]string{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testFailCases {
|
for _, tc := range testFailCases {
|
||||||
var err error
|
|
||||||
var records, endpoints []*endpoint.Endpoint
|
|
||||||
|
|
||||||
records, err = provider.Records(ctx)
|
|
||||||
if errors.Is(err, nil) {
|
|
||||||
endpoints, err = provider.AdjustEndpoints(tc.Endpoints)
|
|
||||||
}
|
|
||||||
if errors.Is(err, nil) {
|
|
||||||
plan := &plan.Plan{
|
|
||||||
Current: records,
|
|
||||||
Desired: endpoints,
|
|
||||||
DomainFilter: endpoint.MatchAllDomainFilters{domainFilter},
|
|
||||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeTXT},
|
|
||||||
}
|
|
||||||
planned := plan.Calculate()
|
|
||||||
err = provider.ApplyChanges(context.Background(), planned.Changes)
|
|
||||||
|
|
||||||
}
|
|
||||||
if e := checkFailed(tc.Name, err, tc.shouldFail); !errors.Is(e, nil) {
|
|
||||||
t.Error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
records, err := provider.Records(ctx)
|
records, err := provider.Records(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("should not fail, %v", err)
|
t.Errorf("should not fail, %v", err)
|
||||||
@ -2522,6 +2259,9 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) {
|
|||||||
for _, ch := range chs {
|
for _, ch := range chs {
|
||||||
actualCustomHostnames[ch.Hostname] = ch.CustomOriginServer
|
actualCustomHostnames[ch.Hostname] = ch.CustomOriginServer
|
||||||
}
|
}
|
||||||
|
if len(actualCustomHostnames) == 0 {
|
||||||
|
actualCustomHostnames = nil
|
||||||
|
}
|
||||||
assert.Equal(t, tc.ExpectedCustomHostnames, actualCustomHostnames, "custom hostnames should be the same")
|
assert.Equal(t, tc.ExpectedCustomHostnames, actualCustomHostnames, "custom hostnames should be the same")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2852,3 +2592,171 @@ 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) {
|
||||||
|
hook := testutils.LogsUnderTestWithLogLevel(log.ErrorLevel, t)
|
||||||
|
|
||||||
|
client := NewMockCloudFlareClient()
|
||||||
|
provider := &CloudFlareProvider{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
changes *plan.Changes
|
||||||
|
customHostnamesEnabled bool
|
||||||
|
errorLogCount int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Create error (custom hostnames enabled)",
|
||||||
|
changes: &plan.Changes{
|
||||||
|
Create: []*endpoint.Endpoint{{
|
||||||
|
DNSName: "bad-create.bar.com",
|
||||||
|
RecordType: "MX",
|
||||||
|
Targets: endpoint.Targets{"not-a-valid-mx"},
|
||||||
|
ProviderSpecific: endpoint.ProviderSpecific{
|
||||||
|
{
|
||||||
|
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
||||||
|
Value: "bad-create-custom.bar.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
customHostnamesEnabled: true,
|
||||||
|
errorLogCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Delete error (custom hostnames enabled)",
|
||||||
|
changes: &plan.Changes{
|
||||||
|
Delete: []*endpoint.Endpoint{{
|
||||||
|
DNSName: "bad-delete.bar.com",
|
||||||
|
RecordType: "MX",
|
||||||
|
Targets: endpoint.Targets{"not-a-valid-mx"},
|
||||||
|
ProviderSpecific: endpoint.ProviderSpecific{
|
||||||
|
{
|
||||||
|
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
||||||
|
Value: "bad-delete-custom.bar.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
customHostnamesEnabled: true,
|
||||||
|
errorLogCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update add/remove error (custom hostnames enabled)",
|
||||||
|
changes: &plan.Changes{
|
||||||
|
UpdateNew: []*endpoint.Endpoint{{
|
||||||
|
DNSName: "bad-update-add.bar.com",
|
||||||
|
RecordType: "MX",
|
||||||
|
Targets: endpoint.Targets{"not-a-valid-mx"},
|
||||||
|
ProviderSpecific: endpoint.ProviderSpecific{
|
||||||
|
{
|
||||||
|
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
||||||
|
Value: "bad-update-add-custom.bar.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
UpdateOld: []*endpoint.Endpoint{{
|
||||||
|
DNSName: "old-bad-update-add.bar.com",
|
||||||
|
RecordType: "MX",
|
||||||
|
Targets: endpoint.Targets{"not-a-valid-mx-but-still-updated"},
|
||||||
|
ProviderSpecific: endpoint.ProviderSpecific{
|
||||||
|
{
|
||||||
|
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
||||||
|
Value: "bad-update-add-custom.bar.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
customHostnamesEnabled: true,
|
||||||
|
errorLogCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update leave error (custom hostnames enabled)",
|
||||||
|
changes: &plan.Changes{
|
||||||
|
UpdateOld: []*endpoint.Endpoint{{
|
||||||
|
DNSName: "bad-update-leave.bar.com",
|
||||||
|
RecordType: "MX",
|
||||||
|
Targets: endpoint.Targets{"not-a-valid-mx"},
|
||||||
|
ProviderSpecific: endpoint.ProviderSpecific{
|
||||||
|
{
|
||||||
|
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
||||||
|
Value: "bad-update-leave-custom.bar.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
UpdateNew: []*endpoint.Endpoint{{
|
||||||
|
DNSName: "bad-update-leave.bar.com",
|
||||||
|
RecordType: "MX",
|
||||||
|
Targets: endpoint.Targets{"not-a-valid-mx"},
|
||||||
|
ProviderSpecific: endpoint.ProviderSpecific{
|
||||||
|
{
|
||||||
|
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
|
||||||
|
Value: "bad-update-leave-custom.bar.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
customHostnamesEnabled: true,
|
||||||
|
errorLogCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Delete error (custom hostnames disabled)",
|
||||||
|
changes: &plan.Changes{
|
||||||
|
Delete: []*endpoint.Endpoint{{
|
||||||
|
DNSName: "bad-delete2.bar.com",
|
||||||
|
RecordType: "MX",
|
||||||
|
Targets: endpoint.Targets{"not-a-valid-mx"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
customHostnamesEnabled: false,
|
||||||
|
errorLogCount: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with custom hostnames enabled and disabled
|
||||||
|
for _, tc := range cases {
|
||||||
|
if tc.customHostnamesEnabled {
|
||||||
|
provider.CustomHostnamesConfig = CustomHostnamesConfig{Enabled: true}
|
||||||
|
} else {
|
||||||
|
provider.CustomHostnamesConfig = CustomHostnamesConfig{Enabled: false}
|
||||||
|
}
|
||||||
|
hook.Reset()
|
||||||
|
err := provider.ApplyChanges(context.Background(), tc.changes)
|
||||||
|
assert.NoError(t, err, "ApplyChanges should not return error for newCloudFlareChange error (it should log and continue)")
|
||||||
|
errorLogCount := 0
|
||||||
|
for _, entry := range hook.Entries {
|
||||||
|
if entry.Level == log.ErrorLevel &&
|
||||||
|
strings.Contains(entry.Message, "failed to create cloudflare change") {
|
||||||
|
errorLogCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.errorLogCount, errorLogCount, "expected error log count for %s", tc.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudFlareProvider_SupportedAdditionalRecordTypes(t *testing.T) {
|
||||||
|
provider := &CloudFlareProvider{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
recordType string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{endpoint.RecordTypeMX, true},
|
||||||
|
{endpoint.RecordTypeA, true},
|
||||||
|
{endpoint.RecordTypeCNAME, true},
|
||||||
|
{endpoint.RecordTypeTXT, true},
|
||||||
|
{endpoint.RecordTypeNS, true},
|
||||||
|
{"SRV", true},
|
||||||
|
{"SPF", false},
|
||||||
|
{"LOC", false},
|
||||||
|
{"UNKNOWN", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.recordType, func(t *testing.T) {
|
||||||
|
result := provider.SupportedAdditionalRecordTypes(tt.recordType)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -20,7 +20,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/digitalocean/godo"
|
"github.com/digitalocean/godo"
|
||||||
@ -302,20 +301,19 @@ func makeDomainEditRequest(domain, name, recordType, data string, ttl int) *godo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if recordType == endpoint.RecordTypeMX {
|
if recordType == endpoint.RecordTypeMX {
|
||||||
priority, domain, err := parseMxTarget(data)
|
mxRecord, err := endpoint.NewMXRecord(data)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
request.Priority = int(priority)
|
|
||||||
request.Data = provider.EnsureTrailingDot(domain)
|
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
"dnsName": name,
|
"dnsName": name,
|
||||||
"recordType": recordType,
|
"recordType": recordType,
|
||||||
"data": data,
|
"data": data,
|
||||||
}).Warn("Unable to parse MX target")
|
}).Warn("Unable to parse MX target")
|
||||||
|
return request
|
||||||
}
|
}
|
||||||
|
request.Priority = int(*mxRecord.GetPriority())
|
||||||
|
request.Data = provider.EnsureTrailingDot(*mxRecord.GetHost())
|
||||||
}
|
}
|
||||||
|
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -661,18 +659,3 @@ func (p *DigitalOceanProvider) ApplyChanges(ctx context.Context, planChanges *pl
|
|||||||
|
|
||||||
return p.submitChanges(ctx, &changes)
|
return p.submitChanges(ctx, &changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMxTarget(mxTarget string) (priority int64, exchange string, err error) {
|
|
||||||
targetParts := strings.SplitN(mxTarget, " ", 2)
|
|
||||||
if len(targetParts) != 2 {
|
|
||||||
return priority, exchange, fmt.Errorf("mx target needs to be of form '10 example.com'")
|
|
||||||
}
|
|
||||||
|
|
||||||
priorityRaw, exchange := targetParts[0], targetParts[1]
|
|
||||||
priority, err = strconv.ParseInt(priorityRaw, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return priority, exchange, fmt.Errorf("invalid priority specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
return priority, exchange, nil
|
|
||||||
}
|
|
||||||
|
@ -108,7 +108,7 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getSupportedTypes() []string {
|
func getSupportedTypes() []string {
|
||||||
return []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}
|
return []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS, endpoint.RecordTypeMX}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (im *TXTRegistry) GetDomainFilter() endpoint.DomainFilterInterface {
|
func (im *TXTRegistry) GetDomainFilter() endpoint.DomainFilterInterface {
|
||||||
|
@ -119,6 +119,8 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
|
|||||||
newEndpointWithOwner("txt.dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("txt.dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""),
|
newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""),
|
||||||
newEndpointWithOwner("txt.aaaa-dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("txt.aaaa-dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""),
|
||||||
|
newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""),
|
||||||
|
newEndpointWithOwner("txt.mx-mail.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expectedRecords := []*endpoint.Endpoint{
|
expectedRecords := []*endpoint.Endpoint{
|
||||||
@ -215,6 +217,14 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
|
|||||||
endpoint.OwnerLabelKey: "owner-2",
|
endpoint.OwnerLabelKey: "owner-2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
DNSName: "mail.test-zone.example.org",
|
||||||
|
Targets: endpoint.Targets{"10 onemail.example.com"},
|
||||||
|
RecordType: endpoint.RecordTypeMX,
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.OwnerLabelKey: "owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
|
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
|
||||||
@ -252,6 +262,8 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) {
|
|||||||
newEndpointWithOwner("dualstack-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("dualstack-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""),
|
newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""),
|
||||||
newEndpointWithOwner("aaaa-dualstack-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("aaaa-dualstack-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""),
|
||||||
|
newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""),
|
||||||
|
newEndpointWithOwner("mx-mail-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expectedRecords := []*endpoint.Endpoint{
|
expectedRecords := []*endpoint.Endpoint{
|
||||||
@ -340,6 +352,14 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) {
|
|||||||
endpoint.OwnerLabelKey: "owner-2",
|
endpoint.OwnerLabelKey: "owner-2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
DNSName: "mail.test-zone.example.org",
|
||||||
|
Targets: endpoint.Targets{"10 onemail.example.com"},
|
||||||
|
RecordType: endpoint.RecordTypeMX,
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.OwnerLabelKey: "owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
|
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
|
||||||
@ -375,6 +395,8 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
|
|||||||
newEndpointWithOwner("dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""),
|
newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""),
|
||||||
newEndpointWithOwner("aaaa-dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("aaaa-dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""),
|
||||||
|
newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""),
|
||||||
|
newEndpointWithOwner("mx-mail.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expectedRecords := []*endpoint.Endpoint{
|
expectedRecords := []*endpoint.Endpoint{
|
||||||
@ -457,6 +479,14 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
|
|||||||
endpoint.OwnerLabelKey: "owner-2",
|
endpoint.OwnerLabelKey: "owner-2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
DNSName: "mail.test-zone.example.org",
|
||||||
|
Targets: endpoint.Targets{"10 onemail.example.com"},
|
||||||
|
RecordType: endpoint.RecordTypeMX,
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.OwnerLabelKey: "owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
|
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
|
||||||
@ -473,6 +503,8 @@ func testTXTRegistryRecordsPrefixedTemplated(t *testing.T) {
|
|||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("foo.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""),
|
newEndpointWithOwner("foo.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""),
|
||||||
newEndpointWithOwner("txt-a.foo.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("txt-a.foo.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
|
newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""),
|
||||||
|
newEndpointWithOwner("txt-mx.mail.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expectedRecords := []*endpoint.Endpoint{
|
expectedRecords := []*endpoint.Endpoint{
|
||||||
@ -484,6 +516,14 @@ func testTXTRegistryRecordsPrefixedTemplated(t *testing.T) {
|
|||||||
endpoint.OwnerLabelKey: "owner",
|
endpoint.OwnerLabelKey: "owner",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
DNSName: "mail.test-zone.example.org",
|
||||||
|
Targets: endpoint.Targets{"10 onemail.example.com"},
|
||||||
|
RecordType: endpoint.RecordTypeMX,
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.OwnerLabelKey: "owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
r, _ := NewTXTRegistry(p, "txt-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
|
r, _ := NewTXTRegistry(p, "txt-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
|
||||||
@ -505,6 +545,8 @@ func testTXTRegistryRecordsSuffixedTemplated(t *testing.T) {
|
|||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("bar.test-zone.example.org", "8.8.8.8", endpoint.RecordTypeCNAME, ""),
|
newEndpointWithOwner("bar.test-zone.example.org", "8.8.8.8", endpoint.RecordTypeCNAME, ""),
|
||||||
newEndpointWithOwner("bartxtcname.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("bartxtcname.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
|
newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""),
|
||||||
|
newEndpointWithOwner("mailtxt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expectedRecords := []*endpoint.Endpoint{
|
expectedRecords := []*endpoint.Endpoint{
|
||||||
@ -516,6 +558,14 @@ func testTXTRegistryRecordsSuffixedTemplated(t *testing.T) {
|
|||||||
endpoint.OwnerLabelKey: "owner",
|
endpoint.OwnerLabelKey: "owner",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
DNSName: "mail.test-zone.example.org",
|
||||||
|
Targets: endpoint.Targets{"10 onemail.example.com"},
|
||||||
|
RecordType: endpoint.RecordTypeMX,
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.OwnerLabelKey: "owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
r, _ := NewTXTRegistry(p, "", "txt%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
|
r, _ := NewTXTRegistry(p, "", "txt%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
|
||||||
|
Loading…
Reference in New Issue
Block a user