Skip to content

Commit e17b5ed

Browse files
arthlrmloiseleur
andauthored
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>
1 parent e324da8 commit e17b5ed

File tree

8 files changed

+632
-508
lines changed

8 files changed

+632
-508
lines changed

docs/sources/mx-record.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# MX record with CRD source
22

33
You can create and manage MX records with the help of [CRD source](../sources/crd.md)
4-
and `DNSEndpoint` CRD. Currently, this feature is only supported by `aws`, `azure`, `google` and `digitalocean` providers.
4+
and `DNSEndpoint` CRD. Currently, this feature is only supported by `aws`, `azure`, `cloudflare`, `digitalocean` and `google` providers.
55

66
In order to start managing MX records you need to set the `--managed-record-types=MX` flag.
77

endpoint/endpoint.go

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ func (ttl TTL) IsConfigured() bool {
7171
// Targets is a representation of a list of targets for an endpoint.
7272
type Targets []string
7373

74+
// MXTarget represents a single MX (Mail Exchange) record target, including its priority and host.
75+
type MXTarget struct {
76+
priority uint16
77+
host string
78+
}
79+
7480
// NewTargets is a convenience method to create a new Targets object from a vararg of strings
7581
func NewTargets(target ...string) Targets {
7682
t := make(Targets, 0, len(target))
@@ -394,22 +400,44 @@ func (e *Endpoint) CheckEndpoint() bool {
394400
return true
395401
}
396402

403+
// NewMXRecord parses a string representation of an MX record target (e.g., "10 mail.example.com")
404+
// and returns an MXTarget struct. Returns an error if the input is invalid.
405+
func NewMXRecord(target string) (*MXTarget, error) {
406+
parts := strings.Fields(strings.TrimSpace(target))
407+
if len(parts) != 2 {
408+
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)
409+
}
410+
411+
priority, err := strconv.ParseUint(parts[0], 10, 16)
412+
if err != nil {
413+
return nil, fmt.Errorf("invalid integer value in target: %s", target)
414+
}
415+
416+
return &MXTarget{
417+
priority: uint16(priority),
418+
host: parts[1],
419+
}, nil
420+
}
421+
422+
// GetPriority returns the priority of the MX record target.
423+
func (m *MXTarget) GetPriority() *uint16 {
424+
return &m.priority
425+
}
426+
427+
// GetHost returns the host of the MX record target.
428+
func (m *MXTarget) GetHost() *string {
429+
return &m.host
430+
}
431+
397432
func (t Targets) ValidateMXRecord() bool {
398433
for _, target := range t {
399-
// MX records must have a preference value to indicate priority, e.g. "10 example.com"
400-
// as per https://www.rfc-editor.org/rfc/rfc974.txt
401-
targetParts := strings.Fields(strings.TrimSpace(target))
402-
if len(targetParts) != 2 {
403-
log.Debugf("Invalid MX record target: %s. MX records must have a preference value to indicate priority, e.g. '10 example.com'", target)
404-
return false
405-
}
406-
preferenceRaw := targetParts[0]
407-
_, err := strconv.ParseUint(preferenceRaw, 10, 16)
434+
_, err := NewMXRecord(target)
408435
if err != nil {
409-
log.Debugf("Invalid SRV record target: %s. Invalid integer value in target.", target)
436+
log.Debugf("Invalid MX record target: %s. %v", target, err)
410437
return false
411438
}
412439
}
440+
413441
return true
414442
}
415443

endpoint/endpoint_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,3 +815,113 @@ func TestPDNScheckEndpoint(t *testing.T) {
815815
assert.Equal(t, tt.expected, actual)
816816
}
817817
}
818+
819+
func TestNewMXTarget(t *testing.T) {
820+
tests := []struct {
821+
description string
822+
target string
823+
expected *MXTarget
824+
expectError bool
825+
}{
826+
{
827+
description: "Valid MX record",
828+
target: "10 example.com",
829+
expected: &MXTarget{priority: 10, host: "example.com"},
830+
expectError: false,
831+
},
832+
{
833+
description: "Invalid MX record with missing priority",
834+
target: "example.com",
835+
expectError: true,
836+
},
837+
{
838+
description: "Invalid MX record with non-integer priority",
839+
target: "abc example.com",
840+
expectError: true,
841+
},
842+
{
843+
description: "Invalid MX record with too many parts",
844+
target: "10 example.com extra",
845+
expectError: true,
846+
},
847+
{
848+
description: "Missing host",
849+
target: "10 ",
850+
expected: nil,
851+
expectError: true,
852+
},
853+
}
854+
855+
for _, tt := range tests {
856+
t.Run(tt.description, func(t *testing.T) {
857+
actual, err := NewMXRecord(tt.target)
858+
if tt.expectError {
859+
assert.Error(t, err)
860+
} else {
861+
assert.NoError(t, err)
862+
assert.Equal(t, tt.expected, actual)
863+
}
864+
})
865+
}
866+
}
867+
868+
func TestCheckEndpoint(t *testing.T) {
869+
tests := []struct {
870+
description string
871+
endpoint Endpoint
872+
expected bool
873+
}{
874+
{
875+
description: "Valid MX record target",
876+
endpoint: Endpoint{
877+
DNSName: "example.com",
878+
RecordType: RecordTypeMX,
879+
Targets: Targets{"10 example.com"},
880+
},
881+
expected: true,
882+
},
883+
{
884+
description: "Invalid MX record target",
885+
endpoint: Endpoint{
886+
DNSName: "example.com",
887+
RecordType: RecordTypeMX,
888+
Targets: Targets{"example.com"},
889+
},
890+
expected: false,
891+
},
892+
{
893+
description: "Valid SRV record target",
894+
endpoint: Endpoint{
895+
DNSName: "_service._tcp.example.com",
896+
RecordType: RecordTypeSRV,
897+
Targets: Targets{"10 5 5060 example.com"},
898+
},
899+
expected: true,
900+
},
901+
{
902+
description: "Invalid SRV record target",
903+
endpoint: Endpoint{
904+
DNSName: "_service._tcp.example.com",
905+
RecordType: RecordTypeSRV,
906+
Targets: Targets{"10 5 example.com"},
907+
},
908+
expected: false,
909+
},
910+
{
911+
description: "Non-MX/SRV record type",
912+
endpoint: Endpoint{
913+
DNSName: "example.com",
914+
RecordType: RecordTypeA,
915+
Targets: Targets{"192.168.1.1"},
916+
},
917+
expected: true,
918+
},
919+
}
920+
921+
for _, tt := range tests {
922+
t.Run(tt.description, func(t *testing.T) {
923+
actual := tt.endpoint.CheckEndpoint()
924+
assert.Equal(t, tt.expected, actual)
925+
})
926+
}
927+
}

0 commit comments

Comments
 (0)