Skip to content

Commit 3494839

Browse files
committed
fix: correct SRV record creation and RFC 2782 trailing dot handling
SRV records were created with the record's own DNS name as the target instead of the actual host from the endpoint target string. This caused all SRV records to point at themselves rather than the intended hosts. Additionally, Rackspace stores SRV target hosts without trailing dots, but external-dns >= 0.21 requires RFC 2782 compliant absolute FQDNs (trailing dot). Add trailing dot normalization in both the read path (convertRecordToEndpoint) and adjustEndpoints so current and desired records match consistently. external-dns compatibility notes: - Versions prior to 0.21 (including 0.18 and 0.20) do not include SRV in the TXT registry's getSupportedTypes(), which prevents the registry from matching srv- prefixed TXT ownership records to their SRV data records. This causes all SRV records to appear unowned and external-dns will not update or delete them. Upgrading to >= 0.21 is required. - CRD-sourced SRV records are broken in external-dns v0.21.0 due to contradictory validation in the CRD source and ValidateSRVRecord. See kubernetes-sigs/external-dns#6357 and the fix in kubernetes-sigs/external-dns#6383. A patched external-dns build is required until that PR is merged. Changes: - Use parts[3] instead of fqdn when building SRV data for Rackspace API - Strip trailing dot before sending to Rackspace (it doesn't use them) - Append trailing dot to SRV targets returned by Records() - Append trailing dot to SRV targets in adjustEndpoints as safety net - Improve SRV test to use distinct target host and validate request body
1 parent 8dbf384 commit 3494839

5 files changed

Lines changed: 61 additions & 10 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@ docker build -t external-dns-rackspace-webhook .
201201
3. **Permission Denied**: Check that your API key has DNS management permissions
202202
4. **TTL Too Low**: The webhook enforces a minimum TTL of 300 seconds
203203

204+
### external-dns Compatibility (SRV Records)
205+
206+
SRV record support requires external-dns v0.21.0 or later:
207+
208+
- **Versions prior to 0.21** do not include SRV in the TXT registry's `getSupportedTypes()`. This prevents the registry from matching `srv-` prefixed TXT ownership records to their SRV data records, causing all SRV records to appear unowned. external-dns will not update or delete them.
209+
210+
- **Version 0.21.0** fixes the ownership matching but introduces contradictory SRV validation in the CRD source. The CRD validator rejects targets with a trailing dot, while `ValidateSRVRecord` requires one per RFC 2782. This makes it impossible to create SRV records via DNSEndpoint CRDs. See [kubernetes-sigs/external-dns#6357](https://github.com/kubernetes-sigs/external-dns/issues/6357) and the fix in [PR #6383](https://github.com/kubernetes-sigs/external-dns/pull/6383).
211+
204212
### Debug Mode
205213

206214
Enable debug logging by setting `LOG_LEVEL` in your values file:

internal/handlers/handler.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package handlers
33
import (
44
"encoding/json"
55
"net/http"
6+
"strings"
67

78
"github.com/charmbracelet/log"
89
"github.com/labstack/echo/v4"
@@ -37,11 +38,10 @@ func (h *Handler) HandleGetRecords(c echo.Context) error {
3738
return c.JSON(http.StatusOK, endpoints)
3839
}
3940

40-
// HandleAdjustEndpoints returns endpoints unchanged. Rackspace does not
41-
// require any provider-specific canonicalization. Transforming endpoints
42-
// here (adding trailing dots, modifying TTL, stripping TXT quotes) caused
43-
// mismatches with what Records() returns and with the TXT registry's
44-
// internal representation, leading to constant update churn.
41+
// HandleAdjustEndpoints normalises provider-specific endpoint details.
42+
// For SRV records, RFC 2782 requires the target host to be an absolute
43+
// FQDN (trailing dot). Sources that omit the dot would be rejected by
44+
// external-dns's ValidateSRVRecord, so we append it here as a safety net.
4545
func (h *Handler) HandleAdjustEndpoints(c echo.Context) error {
4646
defer c.Request().Body.Close()
4747
var endpoints []*endpoint.Endpoint
@@ -50,6 +50,17 @@ func (h *Handler) HandleAdjustEndpoints(c echo.Context) error {
5050
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
5151
}
5252

53+
for _, ep := range endpoints {
54+
if ep.RecordType == "SRV" {
55+
for i, target := range ep.Targets {
56+
parts := strings.SplitN(target, " ", 4)
57+
if len(parts) == 4 && !strings.HasSuffix(parts[3], ".") {
58+
ep.Targets[i] = parts[0] + " " + parts[1] + " " + parts[2] + " " + parts[3] + "."
59+
}
60+
}
61+
}
62+
}
63+
5364
log.Info("POST /adjustendpoints", "count", len(endpoints))
5465
return c.JSON(http.StatusOK, endpoints)
5566
}

internal/providers/consistency_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,23 @@ func TestConvertRecordToEndpoint_SRVFormat(t *testing.T) {
5757
Name: "_mongodb._tcp.myrs.example.com", Type: "SRV",
5858
Data: "0 27017 node1.example.com", TTL: 300, Priority: 10,
5959
},
60-
wantTarget: "10 0 27017 node1.example.com",
60+
wantTarget: "10 0 27017 node1.example.com.",
6161
},
6262
{
6363
name: "zero priority",
6464
record: records.RecordList{
6565
Name: "_sip._tcp.svc.example.com", Type: "SRV",
6666
Data: "5 5060 sip.example.com", TTL: 300, Priority: 0,
6767
},
68-
wantTarget: "0 5 5060 sip.example.com",
68+
wantTarget: "0 5 5060 sip.example.com.",
6969
},
7070
{
7171
name: "high priority value",
7272
record: records.RecordList{
7373
Name: "_http._tcp.web.example.com", Type: "SRV",
7474
Data: "1 443 web.example.com", TTL: 300, Priority: 100,
7575
},
76-
wantTarget: "100 1 443 web.example.com",
76+
wantTarget: "100 1 443 web.example.com.",
7777
},
7878
}
7979

internal/providers/provider_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package providers
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
7+
"io"
68
"net/http"
79
"testing"
810
"time"
@@ -282,7 +284,7 @@ func TestRackspaceProvider_createRecord(t *testing.T) {
282284
endpoint: &endpoint.Endpoint{
283285
DNSName: "_sip._tcp.example.com",
284286
RecordType: "SRV",
285-
Targets: []string{"1 5 5060 _sip._tcp.example.com."},
287+
Targets: []string{"1 5 5060 sipserver.example.com"},
286288
RecordTTL: endpoint.TTL(3600),
287289
Labels: map[string]string{},
288290
},
@@ -331,6 +333,28 @@ func TestRackspaceProvider_createRecord(t *testing.T) {
331333
th.TestMethod(t, r, "POST")
332334
th.TestHeader(t, r, "X-Auth-Token", "cbc36478b0bd8e67e89469c7749d4127")
333335

336+
if !tt.wantErr {
337+
body, _ := io.ReadAll(r.Body)
338+
var payload map[string]interface{}
339+
if err := json.Unmarshal(body, &payload); err != nil {
340+
t.Fatalf("failed to parse request body: %v", err)
341+
}
342+
recs, _ := payload["records"].([]interface{})
343+
if len(recs) > 0 {
344+
rec := recs[0].(map[string]interface{})
345+
if data, ok := rec["data"].(string); ok {
346+
if data != "5 5060 sipserver.example.com" {
347+
t.Errorf("SRV data = %q, want %q", data, "5 5060 sipserver.example.com")
348+
}
349+
}
350+
if priority, ok := rec["priority"].(float64); ok {
351+
if priority != 1 {
352+
t.Errorf("SRV priority = %v, want 1", priority)
353+
}
354+
}
355+
}
356+
}
357+
334358
w.Header().Add("Content-Type", "application/json")
335359
w.WriteHeader(http.StatusCreated)
336360
fmt.Fprint(w, `{

internal/providers/providers.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,14 @@ func convertRecordToEndpoint(record records.RecordList, domainName string) *endp
244244
// target" format that external-dns expects.
245245
if record.Type == "SRV" {
246246
data = fmt.Sprintf("%d %s", record.Priority, record.Data)
247+
// Ensure the target host ends with a dot (RFC 2782) so that the
248+
// value returned here matches what adjustEndpoints produces for
249+
// the desired endpoints. Without this trailing dot external-dns
250+
// ≥ 0.21 rejects the record as invalid.
251+
parts := strings.SplitN(data, " ", 4)
252+
if len(parts) == 4 && !strings.HasSuffix(parts[3], ".") {
253+
data = parts[0] + " " + parts[1] + " " + parts[2] + " " + parts[3] + "."
254+
}
247255
}
248256

249257
return &endpoint.Endpoint{
@@ -288,7 +296,7 @@ func (p *RackspaceProvider) createRecord(ctx context.Context, ep *endpoint.Endpo
288296
return err
289297
}
290298
createOpts.Priority = uint(priority)
291-
createOpts.Data = fmt.Sprintf("%s %s %s", parts[1], parts[2], fqdn)
299+
createOpts.Data = fmt.Sprintf("%s %s %s", parts[1], parts[2], strings.TrimSuffix(parts[3], "."))
292300
}
293301

294302
if _, err := p.getClient(ctx).CreateRecord(ctx, domain.ID, createOpts); err != nil {

0 commit comments

Comments
 (0)