Skip to content

Commit 539e4ee

Browse files
authored
fix: auto delete issue when sync and txt flags are present. (#21)
* fix: auto delete issue when sync and txt flags are present. * fix: use latest version of external dns * fix(webhook): update default port from 2020 to 8888
1 parent ba41414 commit 539e4ee

File tree

5 files changed

+77
-75
lines changed

5 files changed

+77
-75
lines changed

charts/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: external-dns-rackspace
33
description: A Helm chart for deploying external-dns with Rackspace webhook provider
44
type: application
55
version: 0.2.0
6-
appVersion: "v0.18.0"
6+
appVersion: "v0.20.0"
77
keywords:
88
- external-dns
99
- dns

cmd/webhook/main.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"fmt"
55
"log"
6+
"net/url"
67
"os"
78
"strconv"
89
"strings"
@@ -48,16 +49,16 @@ func getStartPort() (int, error) {
4849
}
4950
port, err := strconv.Atoi(portStr)
5051
if err != nil {
51-
return 0, fmt.Errorf("invalid port %s", err.Error())
52+
return 0, fmt.Errorf("invalid port format: %s", portStr)
5253
}
5354
return port, nil
5455
}
5556

5657
func loadConfig() *providers.RackspaceConfig {
5758
config := &providers.RackspaceConfig{
58-
Username: os.Getenv("RACKSPACE_USERNAME"),
59-
APIKey: os.Getenv("RACKSPACE_API_KEY"),
60-
IdentityEndpoint: os.Getenv("RACKSPACE_IDENTITY_ENDPOINT"),
59+
Username: strings.TrimSpace(os.Getenv("RACKSPACE_USERNAME")),
60+
APIKey: strings.TrimSpace(os.Getenv("RACKSPACE_API_KEY")),
61+
IdentityEndpoint: strings.TrimSpace(os.Getenv("RACKSPACE_IDENTITY_ENDPOINT")),
6162
DryRun: false,
6263
LogLevel: "info",
6364
}
@@ -79,8 +80,14 @@ func loadConfig() *providers.RackspaceConfig {
7980
}
8081

8182
// Validate required fields
82-
if config.Username == "" || config.APIKey == "" {
83-
log.Fatal("RACKSPACE_USERNAME and RACKSPACE_API_KEY are required")
83+
if config.Username == "" {
84+
log.Fatal("RACKSPACE_USERNAME is required and cannot be empty")
85+
}
86+
if config.APIKey == "" {
87+
log.Fatal("RACKSPACE_API_KEY is required and cannot be empty")
88+
}
89+
if _, err := url.Parse(config.IdentityEndpoint); err != nil {
90+
log.Fatalf("Invalid RACKSPACE_IDENTITY_ENDPOINT URL: %v", err)
8491
}
8592

8693
return config

internal/handlers/handler.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package handlers
22

33
import (
44
"encoding/json"
5-
"fmt"
65
"net/http"
76
"strings"
87

@@ -31,15 +30,16 @@ func (h *Handler) NegotiationHandler(c echo.Context) error {
3130
func (h *Handler) HandleGetRecords(c echo.Context) error {
3231
endpoints, err := h.provider.Records(c.Request().Context())
3332
if err != nil {
33+
log.Error("Failed to get records", "error", err)
3434
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
3535
}
3636

3737
return c.JSON(http.StatusOK, endpoints)
3838
}
3939

4040
func (h *Handler) HandleAdjustEndpoints(c echo.Context) error {
41+
defer c.Request().Body.Close()
4142
var endpoints []*endpoint.Endpoint
42-
4343
if err := json.NewDecoder(c.Request().Body).Decode(&endpoints); err != nil {
4444
log.Error("Failed to decode input", "error", err)
4545
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
@@ -49,11 +49,14 @@ func (h *Handler) HandleAdjustEndpoints(c echo.Context) error {
4949
adjusted := make([]*endpoint.Endpoint, 0, len(endpoints))
5050

5151
for _, ep := range endpoints {
52+
if ep == nil {
53+
log.Warn("Skipping nil endpoint")
54+
continue
55+
}
5256
if ep == nil || ep.DNSName == "" || len(ep.Targets) == 0 {
5357
log.Warn("Skipping invalid endpoint", "dnsName", ep.DNSName)
5458
continue
5559
}
56-
5760
// Canonicalize DNS name
5861
dnsName := strings.ToLower(strings.TrimSuffix(ep.DNSName, ".")) + "."
5962
if !h.provider.DomainFilter.Match(dnsName) {
@@ -81,25 +84,24 @@ func (h *Handler) HandleAdjustEndpoints(c echo.Context) error {
8184
for _, t := range ep.Targets {
8285
if ep.RecordType == "TXT" {
8386
t = strings.Trim(t, `"`)
84-
t = fmt.Sprintf(`"%s"`, t)
8587
}
8688
targets = append(targets, t)
8789
}
8890

8991
adjusted = append(adjusted, &endpoint.Endpoint{
90-
DNSName: dnsName,
91-
Targets: targets,
92-
RecordType: ep.RecordType,
93-
RecordTTL: ttl,
94-
Labels: ep.Labels,
95-
ProviderSpecific: nil,
92+
DNSName: dnsName,
93+
Targets: targets,
94+
RecordType: ep.RecordType,
95+
RecordTTL: ttl,
96+
Labels: ep.Labels,
9697
})
9798
}
9899

99100
return c.JSON(http.StatusOK, adjusted)
100101
}
101102

102103
func (h *Handler) HandlePostRecords(c echo.Context) error {
104+
defer c.Request().Body.Close()
103105
var changes plan.Changes
104106
if err := json.NewDecoder(c.Request().Body).Decode(&changes); err != nil {
105107
log.Error("Failed to decode input", "error", err)

internal/providers/providers.go

Lines changed: 50 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package providers
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"strings"
@@ -70,13 +71,13 @@ func NewRackspaceProvider(config *RackspaceConfig) (*RackspaceProvider, error) {
7071
}, nil
7172
}
7273

73-
func (p *RackspaceProvider) getClient() ServiceClient {
74+
func (p *RackspaceProvider) getClient(ctx context.Context) ServiceClient {
7475
p.mu.Lock()
7576
defer p.mu.Unlock()
7677
if time.Now().Before(p.tokenExpiry.Add(tokenRefreshBeforeTime)) {
7778
return p.serviceClient
7879
}
79-
clientRaw, tokenExpiry, err := authenticateAndCreateClient(context.Background(), p.authProvider, p.config)
80+
clientRaw, tokenExpiry, err := authenticateAndCreateClient(ctx, p.authProvider, p.config)
8081
if err != nil {
8182
log.Error("Failed to refresh Rackspace token", "error", err)
8283
return p.serviceClient
@@ -107,8 +108,13 @@ func authenticateAndCreateClient(ctx context.Context, authProvider AuthProvider,
107108
tokenExpiry := time.Now().Add(defaultTokenLifetime)
108109
if provider.TokenID != "" {
109110
if authResult, ok := provider.GetAuthResult().(tokens.CreateResult); ok {
110-
if token, err := authResult.ExtractToken(); err == nil && token != nil {
111+
token, err := authResult.ExtractToken()
112+
if err != nil {
113+
log.Warn("Failed to extract token, using default expiry", "error", err)
114+
} else if token != nil {
111115
tokenExpiry = token.ExpiresAt
116+
} else {
117+
log.Warn("Extracted token is nil, using default expiry")
112118
}
113119
}
114120
}
@@ -124,7 +130,7 @@ func authenticateAndCreateClient(ctx context.Context, authProvider AuthProvider,
124130
func (p *RackspaceProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
125131
var endpoints []*endpoint.Endpoint
126132
opts := domains.ListOpts{}
127-
pager := p.getClient().ListDomains(ctx, opts)
133+
pager := p.getClient(ctx).ListDomains(ctx, opts)
128134
start := time.Now()
129135

130136
err := pager.EachPage(ctx, func(ctx context.Context, page pagination.Page) (bool, error) {
@@ -136,12 +142,11 @@ func (p *RackspaceProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
136142
if !p.DomainFilter.Match(domain.Name) {
137143
continue
138144
}
139-
recordOpts := records.ListOpts{}
140-
recordPager := p.getClient().ListRecords(ctx, domain.ID, recordOpts)
145+
recordPager := p.getClient(ctx).ListRecords(ctx, domain.ID, records.ListOpts{})
141146
err := recordPager.EachPage(ctx, func(ctx context.Context, recordPage pagination.Page) (bool, error) {
142147
recordList, err := records.ExtractRecords(recordPage)
143148
if err != nil {
144-
return false, err
149+
return false, fmt.Errorf("failed to extract records: %w", err)
145150
}
146151
for _, record := range recordList {
147152
if ep := convertRecordToEndpoint(record, domain.Name); ep != nil {
@@ -158,7 +163,7 @@ func (p *RackspaceProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
158163
})
159164
log.Debug("Fetched records", "count", len(endpoints), "elapsed", time.Since(start))
160165
if err != nil {
161-
return nil, fmt.Errorf("failed to fetch domains: %v", err)
166+
return nil, fmt.Errorf("failed to fetch domains: %w", err)
162167
}
163168

164169
return endpoints, nil
@@ -167,7 +172,6 @@ func (p *RackspaceProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
167172
// ApplyChanges applies DNS record changes to Rackspace Cloud DNS
168173
func (p *RackspaceProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
169174
var errs []error
170-
171175
log.Info("Applying changes",
172176
"create", len(changes.Create),
173177
"updateNew", len(changes.UpdateNew),
@@ -212,39 +216,19 @@ func convertRecordToEndpoint(record records.RecordList, domainName string) *endp
212216
if record.Type == "NS" || record.Type == "SOA" {
213217
return nil
214218
}
215-
216-
domainName = strings.TrimSuffix(strings.ToLower(domainName), ".")
217-
recordName := strings.TrimSuffix(strings.ToLower(record.Name), ".")
218-
219-
var dnsName string
220-
if recordName == "" || recordName == domainName {
221-
dnsName = domainName + "."
222-
} else if strings.HasSuffix(recordName, "."+domainName) {
223-
dnsName = recordName + "."
224-
} else {
225-
dnsName = recordName + "." + domainName + "."
226-
}
227-
228-
// Normalize TXT data (Rackspace often stores without quotes)
229-
data := record.Data
230-
if record.Type == "TXT" {
231-
data = strings.Trim(data, `"`)
232-
data = fmt.Sprintf(`"%s"`, data)
219+
var labels map[string]string
220+
if record.Type == "TXT" && record.Comment != "" {
221+
if err := json.Unmarshal([]byte(record.Comment), &labels); err != nil {
222+
log.Warn("Failed to unmarshal TXT record labels", "name", record.Name, "comment", record.Comment, "error", err)
223+
}
233224
}
234-
235225
ep := &endpoint.Endpoint{
236-
DNSName: dnsName,
237-
RecordType: record.Type,
238-
Targets: []string{data},
239-
ProviderSpecific: nil,
240-
}
241-
242-
if record.TTL != 0 {
243-
ep.RecordTTL = endpoint.TTL(record.TTL)
244-
} else {
245-
ep.RecordTTL = endpoint.TTL(300) // default to 300s if API didn’t return
226+
DNSName: record.Name,
227+
RecordType: record.Type,
228+
Targets: []string{record.Data},
229+
RecordTTL: endpoint.TTL(record.TTL),
230+
Labels: labels,
246231
}
247-
248232
return ep
249233
}
250234

@@ -255,19 +239,21 @@ func (p *RackspaceProvider) createRecord(ctx context.Context, ep *endpoint.Endpo
255239
}
256240
fqdn := strings.TrimSuffix(strings.ToLower(ep.DNSName), ".")
257241
for _, target := range ep.Targets {
258-
createOpts := records.CreateOpts{
259-
Name: fqdn,
260-
Type: ep.RecordType,
261-
Data: target,
262-
}
263-
if ep.RecordTTL.IsConfigured() {
264-
ttl := uint(ep.RecordTTL)
265-
if ttl < 300 {
266-
ttl = 300
242+
var labels string
243+
if ep.RecordType == "TXT" {
244+
target = strings.Trim(target, `"`)
245+
if len(ep.Labels) > 0 {
246+
b, _ := json.Marshal(ep.Labels)
247+
labels = string(b)
267248
}
268-
createOpts.TTL = ttl
269249
}
270-
if _, err := p.getClient().CreateRecord(ctx, domain.ID, createOpts); err != nil {
250+
createOpts := records.CreateOpts{
251+
Name: fqdn,
252+
Type: ep.RecordType,
253+
Data: target,
254+
Comment: labels,
255+
}
256+
if _, err := p.getClient(ctx).CreateRecord(ctx, domain.ID, createOpts); err != nil {
271257
return fmt.Errorf("failed to create record %s: %v", ep.DNSName, err)
272258
}
273259
log.Info("Created record", "dnsName", ep.DNSName, "type", ep.RecordType, "target", target)
@@ -297,8 +283,11 @@ func (p *RackspaceProvider) deleteRecord(ctx context.Context, endpoint *endpoint
297283
}
298284

299285
func (p *RackspaceProvider) deleteRecordByName(ctx context.Context, domain *domains.DomainList, dnsName, recordType string) error {
286+
if domain == nil {
287+
return fmt.Errorf("domain cannot be nil")
288+
}
300289
wantName := strings.TrimSuffix(strings.ToLower(dnsName), ".")
301-
pager := p.getClient().ListRecords(ctx, domain.ID, records.ListOpts{})
290+
pager := p.getClient(ctx).ListRecords(ctx, domain.ID, records.ListOpts{})
302291

303292
var errs []error
304293
err := pager.EachPage(ctx, func(ctx context.Context, page pagination.Page) (bool, error) {
@@ -310,8 +299,8 @@ func (p *RackspaceProvider) deleteRecordByName(ctx context.Context, domain *doma
310299
for _, rec := range recordList {
311300
gotName := strings.TrimSuffix(strings.ToLower(rec.Name), ".")
312301
if gotName == wantName && strings.EqualFold(rec.Type, recordType) {
313-
if e := p.getClient().DeleteRecord(ctx, domain.ID, rec.ID); e != nil {
314-
errs = append(errs, fmt.Errorf("failed to delete record %s: %v", rec.Name, e))
302+
if e := p.getClient(ctx).DeleteRecord(ctx, domain.ID, rec.ID); e != nil {
303+
errs = append(errs, fmt.Errorf("failed to delete record %s: %w", rec.Name, e))
315304
} else {
316305
log.Info("Deleted record", "dnsName", rec.Name, "type", recordType)
317306
}
@@ -320,24 +309,27 @@ func (p *RackspaceProvider) deleteRecordByName(ctx context.Context, domain *doma
320309
return true, nil
321310
})
322311
if err != nil {
323-
return fmt.Errorf("failed to list records: %v", err)
312+
return fmt.Errorf("failed to list records: %w", err)
324313
}
325314
if len(errs) > 0 {
326-
return fmt.Errorf("errors during deletion: %v", errs)
315+
return errors.Join(errs...)
327316
}
328317
return nil
329318
}
330319

331320
func (p *RackspaceProvider) findDomain(ctx context.Context, dnsName string) (*domains.DomainList, error) {
321+
if dnsName == "" {
322+
return nil, fmt.Errorf("DNS name cannot be empty")
323+
}
332324
dnsName = strings.TrimSuffix(strings.ToLower(dnsName), ".")
333325
opts := domains.ListOpts{}
334-
pager := p.getClient().ListDomains(ctx, opts)
326+
pager := p.getClient(ctx).ListDomains(ctx, opts)
335327

336328
var bestMatch *domains.DomainList
337329
err := pager.EachPage(ctx, func(ctx context.Context, page pagination.Page) (bool, error) {
338330
domainList, err := domains.ExtractDomains(page)
339331
if err != nil {
340-
return false, err
332+
return false, fmt.Errorf("failed to extract domains: %w", err)
341333
}
342334
for _, domain := range domainList {
343335
domainName := strings.TrimSuffix(strings.ToLower(domain.Name), ".")
@@ -351,7 +343,7 @@ func (p *RackspaceProvider) findDomain(ctx context.Context, dnsName string) (*do
351343
})
352344

353345
if err != nil {
354-
return nil, fmt.Errorf("failed to list domains: %v", err)
346+
return nil, fmt.Errorf("failed to list domains: %w", err)
355347
}
356348
if bestMatch == nil {
357349
return nil, fmt.Errorf("no matching domain found for %s", dnsName)

internal/routes/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
)
99

1010
func ConfigureRoutes(e *echo.Echo, h *handlers.Handler) {
11+
e.Use(echoMiddleware.Recover())
1112
e.Pre(echoMiddleware.RemoveTrailingSlash())
1213
e.Use(middleware.ExternalDNSContentTypeMiddleware)
1314

0 commit comments

Comments
 (0)