Skip to content

Commit 80f510c

Browse files
MX records support backport to Legacy DNS (#76)
* Updated README, tests and DNS code * Fixed format * Fixed ineffectual assignment
1 parent 016ac81 commit 80f510c

File tree

6 files changed

+452
-33
lines changed

6 files changed

+452
-33
lines changed

README.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# ExternalDNS - UNOFFICIAL Hetzner Webhook
22

3-
⚠️ **This software is experimental.** ⚠️
3+
> [!WARNING]
4+
> **This software is experimental.**
45
5-
ℹ️ The latest version is **v0.9.1**.
6+
> [!NOTE]
7+
> The latest version is **v0.9.2**.
68
79
[ExternalDNS](https://github.com/kubernetes-sigs/external-dns) is a Kubernetes
810
add-on for automatically DNS records for Kubernetes services using different
@@ -13,8 +15,9 @@ you to manage your Hetzner domains inside your kubernetes cluster.
1315

1416
This webhook supports both the old DNS API and the new Cloud DNS interface.
1517

16-
ℹ️ If you are upgrading to **0.8.x** from previous versions read the
17-
[Upgrading from previous versions](#upgrading-from-previous-versions) section.
18+
> [!TIP]
19+
> If you are upgrading to **0.8.x** from previous versions read the
20+
> [Upgrading from previous versions](#upgrading-from-previous-versions) section.
1821
1922

2023
## Requirements
@@ -77,7 +80,7 @@ provider:
7780
webhook:
7881
image:
7982
repository: ghcr.io/mconfalonieri/external-dns-hetzner-webhook
80-
tag: v0.9.1
83+
tag: v0.9.2
8184
env:
8285
- name: HETZNER_API_KEY
8386
valueFrom:
@@ -105,11 +108,14 @@ And then:
105108
106109
```shell
107110
# install external-dns with helm
108-
helm install external-dns-hetzner external-dns/external-dns -f external-dns-hetzner-values.yaml --version 0.15.0 -n external-dns
111+
helm install external-dns-hetzner external-dns/external-dns -f external-dns-hetzner-values.yaml -n external-dns
109112
```
110113

111114
### Using the Bitnami chart
112115

116+
> [!NOTE]
117+
> The Bitnami distribution model changed and most features are now paid for.
118+
113119
Skip this step if you already have the Bitnami repository added:
114120

115121
```shell
@@ -134,7 +140,7 @@ extraArgs:
134140

135141
sidecars:
136142
- name: hetzner-webhook
137-
image: ghcr.io/mconfalonieri/external-dns-hetzner-webhook:v0.9.1
143+
image: ghcr.io/mconfalonieri/external-dns-hetzner-webhook:v0.9.2
138144
ports:
139145
- containerPort: 8888
140146
name: webhook
@@ -219,6 +225,9 @@ repeated failed attempts to retrieve the records. If this parameter is set to
219225
a value strictly greater than zero, the webhook will shut down after the
220226
configured number of attempts. The default is `-1` (shutdown disabled).
221227

228+
MX records are now supported for both the Cloud API (since **v0.9.1**) and the
229+
Legacy DNS API (since **v0.9.2**).
230+
222231
### 0.7.x to 0.8.x
223232

224233
The configuration is still compatible, however some changes were introduced that
@@ -270,8 +279,9 @@ Hetzner DNS API.
270279
| SLASH_ESC_SEQ | Escape sequence for label annotations | Default: `--slash--` |
271280
| MAX_FAIL_COUNT | Number of failed calls before shutdown | Default: `-1` (disabled) |
272281

273-
Please notice that when **USE_CLOUD_API** is set to `true`, the token stored in
274-
**HETZNER_API_KEY** must be a Hetzner Cloud token, NOT the classic DNS one.
282+
> [!IMPORTANT]
283+
> Please notice that when **USE_CLOUD_API** is set to `true`, the token stored
284+
> in **HETZNER_API_KEY** must be a Hetzner Cloud token, NOT the classic DNS one.
275285

276286
### Test and debug
277287

README.md.gotmpl

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# ExternalDNS - UNOFFICIAL Hetzner Webhook
22

3-
⚠️ **This software is experimental.** ⚠️
3+
> [!WARNING]
4+
> **This software is experimental.**
45

5-
ℹ️ The latest version is **{{ .Version }}**.
6+
> [!NOTE]
7+
> The latest version is **{{ .Version }}**.
68

79
[ExternalDNS](https://github.com/kubernetes-sigs/external-dns) is a Kubernetes
810
add-on for automatically DNS records for Kubernetes services using different
@@ -13,8 +15,9 @@ you to manage your Hetzner domains inside your kubernetes cluster.
1315

1416
This webhook supports both the old DNS API and the new Cloud DNS interface.
1517

16-
ℹ️ If you are upgrading to **0.8.x** from previous versions read the
17-
[Upgrading from previous versions](#upgrading-from-previous-versions) section.
18+
> [!TIP]
19+
> If you are upgrading to **0.8.x** from previous versions read the
20+
> [Upgrading from previous versions](#upgrading-from-previous-versions) section.
1821

1922

2023
## Requirements
@@ -105,11 +108,14 @@ And then:
105108

106109
```shell
107110
# install external-dns with helm
108-
helm install external-dns-hetzner external-dns/external-dns -f external-dns-hetzner-values.yaml --version 0.15.0 -n external-dns
111+
helm install external-dns-hetzner external-dns/external-dns -f external-dns-hetzner-values.yaml -n external-dns
109112
```
110113

111114
### Using the Bitnami chart
112115

116+
> [!NOTE]
117+
> The Bitnami distribution model changed and most features are now paid for.
118+
113119
Skip this step if you already have the Bitnami repository added:
114120

115121
```shell
@@ -219,6 +225,9 @@ repeated failed attempts to retrieve the records. If this parameter is set to
219225
a value strictly greater than zero, the webhook will shut down after the
220226
configured number of attempts. The default is `-1` (shutdown disabled).
221227

228+
MX records are now supported for both the Cloud API (since **v0.9.1**) and the
229+
Legacy DNS API (since **v0.9.2**).
230+
222231
### 0.7.x to 0.8.x
223232

224233
The configuration is still compatible, however some changes were introduced that
@@ -270,8 +279,9 @@ Hetzner DNS API.
270279
| SLASH_ESC_SEQ | Escape sequence for label annotations | Default: `--slash--` |
271280
| MAX_FAIL_COUNT | Number of failed calls before shutdown | Default: `-1` (disabled) |
272281

273-
Please notice that when **USE_CLOUD_API** is set to `true`, the token stored in
274-
**HETZNER_API_KEY** must be a Hetzner Cloud token, NOT the classic DNS one.
282+
> [!IMPORTANT]
283+
> Please notice that when **USE_CLOUD_API** is set to `true`, the token stored
284+
> in **HETZNER_API_KEY** must be a Hetzner Cloud token, NOT the classic DNS one.
275285

276286
### Test and debug
277287

internal/hetzner/dns/change_processors.go

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package hetznerdns
2121

2222
import (
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/
3539
func 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.
4896
func 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.
100147
func 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.
201246
func 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

Comments
 (0)