2020package hetznerdns
2121
2222import (
23+ "strconv"
2324 "strings"
2425
2526 "sigs.k8s.io/external-dns/endpoint"
@@ -32,6 +33,9 @@ import (
3233// adjustCNAMETarget fixes local CNAME targets. It ensures that targets
3334// matching the domain are stripped of the domain parts and that "external"
3435// targets end with a dot.
36+ //
37+ // Hetzner DNS convention: local hostnames have NO trailing dot, external DO.
38+ // See: https://docs.hetzner.com/dns-console/dns/record-types/mx-record/
3539func adjustCNAMETarget (domain string , target string ) string {
3640 adjustedTarget := target
3741 if strings .HasSuffix (target , "." + domain ) {
@@ -44,6 +48,50 @@ func adjustCNAMETarget(domain string, target string) string {
4448 return adjustedTarget
4549}
4650
51+ // adjustMXTarget adjusts MX record target to Hetzner DNS format.
52+ // MX target format from ExternalDNS: "10 mail.example.com"
53+ // Hetzner expects: "10 mail" (local) or "10 mail.other.com." (external with dot)
54+ func adjustMXTarget (domain string , target string ) string {
55+ parts := strings .SplitN (target , " " , 2 )
56+ if len (parts ) != 2 {
57+ log .WithFields (log.Fields {
58+ "target" : target ,
59+ }).Warn ("MX target has invalid format (expected 'priority hostname')" )
60+ return target
61+ }
62+ priority := parts [0 ]
63+ host := parts [1 ]
64+
65+ // Validate priority is numeric
66+ if _ , err := strconv .Atoi (priority ); err != nil {
67+ log .WithFields (log.Fields {
68+ "target" : target ,
69+ "priority" : priority ,
70+ }).Warn ("MX priority is not a valid integer" )
71+ return target
72+ }
73+
74+ // Handle apex record (host equals domain)
75+ hostNoDot := strings .TrimSuffix (host , "." )
76+ if hostNoDot == domain {
77+ return priority + " @"
78+ }
79+
80+ // Use existing CNAME logic for hostname
81+ return priority + " " + adjustCNAMETarget (domain , host )
82+ }
83+
84+ // adjustTarget adjusts the target depending on its type
85+ func adjustTarget (domain , recordType , target string ) string {
86+ switch recordType {
87+ case "CNAME" :
88+ target = adjustCNAMETarget (domain , target )
89+ case "MX" :
90+ target = adjustMXTarget (domain , target )
91+ }
92+ return target
93+ }
94+
4795// processCreateActionsByZone processes the create actions for one zone.
4896func processCreateActionsByZone (zoneID , zoneName string , records []hdns.Record , endpoints []* endpoint.Endpoint , changes * hetznerChanges ) {
4997 for _ , ep := range endpoints {
@@ -58,9 +106,7 @@ func processCreateActionsByZone(zoneID, zoneName string, records []hdns.Record,
58106 }
59107
60108 for _ , target := range ep .Targets {
61- if ep .RecordType == "CNAME" {
62- target = adjustCNAMETarget (zoneName , target )
63- }
109+ target = adjustTarget (zoneName , ep .RecordType , target )
64110 opts := & hdns.RecordCreateOpts {
65111 Name : makeEndpointName (zoneName , ep .DNSName ),
66112 Ttl : getEndpointTTL (ep ),
@@ -97,12 +143,11 @@ func processCreateActions(
97143 }
98144}
99145
146+ // processUpdateEndpoint processes the update requests for an endpoint.
100147func processUpdateEndpoint (zoneID , zoneName string , matchingRecordsByTarget map [string ]hdns.Record , ep * endpoint.Endpoint , changes * hetznerChanges ) {
101148 // Generate create and delete actions based on existence of a record for each target.
102149 for _ , target := range ep .Targets {
103- if ep .RecordType == "CNAME" {
104- target = adjustCNAMETarget (zoneName , target )
105- }
150+ target = adjustTarget (zoneName , ep .RecordType , target )
106151 if record , ok := matchingRecordsByTarget [target ]; ok {
107152 opts := & hdns.RecordUpdateOpts {
108153 Name : makeEndpointName (zoneName , ep .DNSName ),
@@ -200,12 +245,9 @@ func processUpdateActions(
200245// targetsMatch determines if a record matches one of the endpoint's targets.
201246func targetsMatch (record hdns.Record , ep * endpoint.Endpoint ) bool {
202247 for _ , t := range ep .Targets {
203- endpointTarget := t
204248 recordTarget := record .Value
205- if ep .RecordType == endpoint .RecordTypeCNAME {
206- domain := record .Zone .Name
207- endpointTarget = adjustCNAMETarget (domain , t )
208- }
249+ domain := record .Zone .Name
250+ endpointTarget := adjustTarget (domain , ep .RecordType , t )
209251 if endpointTarget == recordTarget {
210252 return true
211253 }
0 commit comments