Skip to content

Commit 3af0375

Browse files
Merge pull request #31 from rackerlabs/fix/srv-record-creation
fix: correct SRV record creation and RFC 2782 trailing dot handling
2 parents 8dbf384 + 3494839 commit 3af0375

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)