Skip to content

Commit da3ed94

Browse files
authored
Update to use libdns 1.x (#8)
Migrate to libdns v1.1.0 - Update libdns dependency to v1.1.0 - Replace libdns.Record with specific record types - Implement new SetRecords atomicity semantics - Update tests for new API
1 parent 35c6412 commit da3ed94

File tree

6 files changed

+271
-154
lines changed

6 files changed

+271
-154
lines changed

client.go

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import (
55
"crypto/tls"
66
"encoding/json"
77
"fmt"
8-
"github.com/libdns/libdns"
98
"io"
109
"log"
1110
"net/http"
1211
"net/url"
1312
"runtime"
1413
"strconv"
1514
"strings"
15+
16+
"github.com/libdns/libdns"
1617
)
1718

1819
func (p *Provider) getZoneRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
@@ -80,7 +81,8 @@ func (p *Provider) getZoneRecords(ctx context.Context, zone string) ([]libdns.Re
8081
if err != nil {
8182
switch err {
8283
case ErrUnsupported:
83-
fmt.Printf("[%s] unsupported record conversion of type %v: %v\n", p.caller(callerSkipDepth), libDnsRecord.Type, libDnsRecord.Name)
84+
rr := libDnsRecord.RR()
85+
fmt.Printf("[%s] unsupported record conversion of type %v: %v\n", p.caller(callerSkipDepth), rr.Type, rr.Name)
8486
continue
8587
default:
8688
return nil, err
@@ -99,7 +101,7 @@ func (p *Provider) appendZoneRecord(ctx context.Context, zone string, record lib
99101
reqURL, err := url.Parse(p.ServerURL)
100102
if err != nil {
101103
fmt.Printf("[%s] failed to parse server url: %v\n", p.caller(2), err)
102-
return libdns.Record{}, err
104+
return nil, err
103105
}
104106

105107
reqURL.Path = "/CMD_API_DNS_CONTROL"
@@ -110,24 +112,25 @@ func (p *Provider) appendZoneRecord(ctx context.Context, zone string, record lib
110112
queryString.Set("full_mx_records", "yes")
111113
queryString.Set("allow_dns_underscore", "yes")
112114
queryString.Set("domain", zone)
113-
queryString.Set("type", record.Type)
114-
queryString.Set("name", record.Name)
115-
queryString.Set("value", record.Value)
116115

117-
if record.Type != "NS" {
118-
queryString.Set("ttl", strconv.Itoa(int(record.TTL.Seconds())))
116+
rr := record.RR()
117+
queryString.Set("type", rr.Type)
118+
queryString.Set("name", rr.Name)
119+
queryString.Set("value", rr.Data)
120+
121+
if rr.Type != "NS" {
122+
queryString.Set("ttl", strconv.Itoa(int(rr.TTL.Seconds())))
119123
}
120124

121125
reqURL.RawQuery = queryString.Encode()
122126

123127
err = p.executeRequest(ctx, http.MethodGet, reqURL.String())
124128
if err != nil {
125-
return libdns.Record{}, err
129+
return nil, err
126130
}
127131

128-
record.ID = fmt.Sprintf("name=%v&value=%v", record.Name, record.Value)
129-
130-
return record, nil
132+
rr.Data = fmt.Sprintf("name=%v&value=%v", rr.Name, rr.Data)
133+
return &rr, nil
131134
}
132135

133136
func (p *Provider) setZoneRecord(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) {
@@ -137,7 +140,7 @@ func (p *Provider) setZoneRecord(ctx context.Context, zone string, record libdns
137140
reqURL, err := url.Parse(p.ServerURL)
138141
if err != nil {
139142
fmt.Printf("[%s] failed to parse server url: %v\n", p.caller(2), err)
140-
return libdns.Record{}, err
143+
return nil, err
141144
}
142145

143146
reqURL.Path = "/CMD_API_DNS_CONTROL"
@@ -146,18 +149,21 @@ func (p *Provider) setZoneRecord(ctx context.Context, zone string, record libdns
146149
queryString.Set("action", "edit")
147150
queryString.Set("json", "yes")
148151
queryString.Set("domain", zone)
149-
queryString.Set("type", record.Type)
150-
queryString.Set("name", record.Name)
151-
queryString.Set("value", record.Value)
152152

153-
if record.Type != "NS" {
154-
queryString.Set("ttl", strconv.Itoa(int(record.TTL.Seconds())))
153+
rr := record.RR()
154+
queryString.Set("type", rr.Type)
155+
queryString.Set("name", rr.Name)
156+
queryString.Set("value", rr.Data)
157+
158+
if rr.Type != "NS" {
159+
queryString.Set("ttl", strconv.Itoa(int(rr.TTL.Seconds())))
155160
}
156161

157162
existingRecords, _ := p.getZoneRecords(ctx, zone)
158163
var existingRecordIndex = -1
159164
for i := range existingRecords {
160-
if existingRecords[i].Name == record.Name && existingRecords[i].Type == record.Type {
165+
existingRR := existingRecords[i].RR()
166+
if existingRR.Name == rr.Name && existingRR.Type == rr.Type {
161167
existingRecordIndex = i
162168
break
163169
}
@@ -166,21 +172,20 @@ func (p *Provider) setZoneRecord(ctx context.Context, zone string, record libdns
166172
// If we're not -1, we found a matching existing record. This changes the API call
167173
// from create only to edit.
168174
if existingRecordIndex != -1 {
169-
editKey := fmt.Sprintf("%vrecs0", strings.ToLower(record.Type))
170-
editValue := existingRecords[existingRecordIndex].ID
175+
editKey := fmt.Sprintf("%vrecs0", strings.ToLower(rr.Type))
176+
editValue := existingRecords[existingRecordIndex].RR().Data
171177
queryString.Set(editKey, editValue)
172178
}
173179

174180
reqURL.RawQuery = queryString.Encode()
175181

176182
err = p.executeRequest(ctx, http.MethodGet, reqURL.String())
177183
if err != nil {
178-
return libdns.Record{}, err
184+
return nil, err
179185
}
180186

181-
record.ID = fmt.Sprintf("name=%v&value=%v", record.Name, record.Value)
182-
183-
return record, nil
187+
rr.Data = fmt.Sprintf("name=%v&value=%v", rr.Name, rr.Data)
188+
return &rr, nil
184189
}
185190

186191
func (p *Provider) deleteZoneRecord(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) {
@@ -190,7 +195,7 @@ func (p *Provider) deleteZoneRecord(ctx context.Context, zone string, record lib
190195
reqURL, err := url.Parse(p.ServerURL)
191196
if err != nil {
192197
fmt.Printf("[%s] failed to parse server url: %v\n", p.caller(2), err)
193-
return libdns.Record{}, err
198+
return nil, err
194199
}
195200

196201
reqURL.Path = "/CMD_API_DNS_CONTROL"
@@ -200,15 +205,16 @@ func (p *Provider) deleteZoneRecord(ctx context.Context, zone string, record lib
200205
queryString.Set("json", "yes")
201206
queryString.Set("domain", zone)
202207

203-
editKey := fmt.Sprintf("%vrecs0", strings.ToLower(record.Type))
204-
editValue := fmt.Sprintf("name=%v&value=%v", record.Name, record.Value)
208+
rr := record.RR()
209+
editKey := fmt.Sprintf("%vrecs0", strings.ToLower(rr.Type))
210+
editValue := fmt.Sprintf("name=%v&value=%v", rr.Name, rr.Data)
205211
queryString.Set(editKey, editValue)
206212

207213
reqURL.RawQuery = queryString.Encode()
208214

209215
err = p.executeRequest(ctx, http.MethodGet, reqURL.String())
210216
if err != nil {
211-
return libdns.Record{}, err
217+
return nil, err
212218
}
213219

214220
return record, nil

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ module github.com/libdns/directadmin
22

33
go 1.18
44

5-
require github.com/libdns/libdns v0.2.2
5+
require github.com/libdns/libdns v1.1.0
66

77
require github.com/joho/godotenv v1.5.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
22
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
3-
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
4-
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
3+
github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU=
4+
github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=

models.go

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ package directadmin
33
import (
44
"errors"
55
"fmt"
6-
"github.com/libdns/libdns"
76
"strconv"
87
"strings"
98
"time"
9+
10+
"github.com/libdns/libdns"
1011
)
1112

1213
type daZone struct {
@@ -40,40 +41,74 @@ type daRecord struct {
4041
var ErrUnsupported = errors.New("unsupported record type")
4142

4243
func (r daRecord) libdnsRecord(zone string) (libdns.Record, error) {
43-
record := libdns.Record{
44-
ID: r.Combined,
45-
Type: r.Type,
44+
var ttl time.Duration
45+
if len(r.TTL) > 0 {
46+
ttlVal, err := strconv.Atoi(r.TTL)
47+
if err != nil {
48+
return nil, fmt.Errorf("failed to parse TTL for %v: %v", r.Name, err)
49+
}
50+
ttl = time.Duration(ttlVal) * time.Second
51+
}
52+
53+
rr := libdns.RR{
4654
Name: r.Name,
55+
Type: r.Type,
56+
TTL: ttl,
57+
Data: r.Value,
4758
}
4859

4960
switch r.Type {
5061
case "MX":
5162
splits := strings.Split(r.Value, " ")
52-
53-
priority, err := strconv.Atoi(splits[0])
54-
if err != nil {
55-
return record, fmt.Errorf("failed to parse MX priority for %v: %v", r.Name, err)
63+
if len(splits) >= 2 {
64+
priority, err := strconv.Atoi(splits[0])
65+
if err == nil {
66+
return &libdns.MX{
67+
Name: r.Name,
68+
TTL: ttl,
69+
Preference: uint16(priority),
70+
Target: fmt.Sprintf("%v.%v", splits[1], zone),
71+
}, nil
72+
}
5673
}
57-
58-
record.Priority = uint(priority)
59-
record.Value = fmt.Sprintf("%v.%v", splits[1], zone)
74+
return &rr, nil
6075
case "SRV":
61-
return record, ErrUnsupported
62-
case "URI":
63-
return record, ErrUnsupported
64-
default:
65-
record.Value = r.Value
66-
}
76+
// Parse SRV record format: "priority weight port target"
77+
splits := strings.Split(r.Value, " ")
78+
if len(splits) >= 4 {
79+
priority, err1 := strconv.Atoi(splits[0])
80+
weight, err2 := strconv.Atoi(splits[1])
81+
port, err3 := strconv.Atoi(splits[2])
82+
if err1 == nil && err2 == nil && err3 == nil {
83+
// Extract service and transport from the name
84+
// SRV names are typically in format: _service._transport.name
85+
nameParts := strings.Split(r.Name, ".")
86+
if len(nameParts) >= 3 && strings.HasPrefix(nameParts[0], "_") && strings.HasPrefix(nameParts[1], "_") {
87+
service := strings.TrimPrefix(nameParts[0], "_")
88+
transport := strings.TrimPrefix(nameParts[1], "_")
89+
// Reconstruct the base name (without service and transport prefixes)
90+
baseName := strings.Join(nameParts[2:], ".")
6791

68-
if len(r.TTL) > 0 {
69-
ttl, err := strconv.Atoi(r.TTL)
70-
if err != nil {
71-
return record, fmt.Errorf("failed to parse TTL for %v: %v", r.Name, err)
92+
return &libdns.SRV{
93+
Service: service,
94+
Transport: transport,
95+
Name: baseName,
96+
TTL: ttl,
97+
Priority: uint16(priority),
98+
Weight: uint16(weight),
99+
Port: uint16(port),
100+
Target: splits[3],
101+
}, nil
102+
}
103+
}
72104
}
73-
record.TTL = time.Duration(ttl) * time.Second
105+
// Fall back to generic RR if parsing fails
106+
return &rr, nil
107+
case "URI":
108+
return nil, ErrUnsupported
109+
default:
110+
return &rr, nil
74111
}
75-
76-
return record, nil
77112
}
78113

79114
type daResponse struct {

provider.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package directadmin
44

55
import (
66
"context"
7+
"fmt"
78
"strings"
89
"sync"
910

@@ -78,14 +79,26 @@ func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns
7879
zone = strings.TrimSuffix(zone, ".")
7980

8081
var updated []libdns.Record
82+
var errors []error
83+
8184
for _, rec := range records {
8285
result, err := p.setZoneRecord(ctx, zone, rec)
8386
if err != nil {
84-
return nil, err
87+
errors = append(errors, err)
88+
continue
8589
}
8690
updated = append(updated, result)
8791
}
8892

93+
if len(errors) > 0 {
94+
if len(updated) == 0 {
95+
// No records were updated, return AtomicErr
96+
return nil, libdns.AtomicErr(fmt.Errorf("all records failed to update: %v", errors))
97+
}
98+
// Some records were updated, return a combined error
99+
return updated, fmt.Errorf("partial update failed: %v", errors)
100+
}
101+
89102
return updated, nil
90103
}
91104

0 commit comments

Comments
 (0)