-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathprovider.go
381 lines (307 loc) · 11.5 KB
/
provider.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
package exoscale
import (
"context"
"errors"
"fmt"
"time"
"strings"
"sync"
"github.com/libdns/libdns"
egoscale "github.com/exoscale/egoscale/v3"
"github.com/exoscale/egoscale/v3/credentials"
)
// Provider facilitates DNS record manipulation with Exoscale.
type Provider struct {
// Exoscale API Key (required)
APIKey string `json:"api_key,omitempty"`
// Exoscale API Secret (required)
APISecret string `json:"api_secret,omitempty"`
client *egoscale.Client
mutex sync.Mutex
}
// Create a map to store the string to CreateDNSDomainRecordRequestType mappings
var dnsRecordTypeMap = map[string]egoscale.CreateDNSDomainRecordRequestType{
"NS": egoscale.CreateDNSDomainRecordRequestTypeNS,
"CAA": egoscale.CreateDNSDomainRecordRequestTypeCAA,
"NAPTR": egoscale.CreateDNSDomainRecordRequestTypeNAPTR,
"POOL": egoscale.CreateDNSDomainRecordRequestTypePOOL,
"A": egoscale.CreateDNSDomainRecordRequestTypeA,
"HINFO": egoscale.CreateDNSDomainRecordRequestTypeHINFO,
"CNAME": egoscale.CreateDNSDomainRecordRequestTypeCNAME,
"SSHFP": egoscale.CreateDNSDomainRecordRequestTypeSSHFP,
"SRV": egoscale.CreateDNSDomainRecordRequestTypeSRV,
"AAAA": egoscale.CreateDNSDomainRecordRequestTypeAAAA,
"MX": egoscale.CreateDNSDomainRecordRequestTypeMX,
"TXT": egoscale.CreateDNSDomainRecordRequestTypeTXT,
"ALIAS": egoscale.CreateDNSDomainRecordRequestTypeALIAS,
"URL": egoscale.CreateDNSDomainRecordRequestTypeURL,
"SPF": egoscale.CreateDNSDomainRecordRequestTypeSPF,
}
// Function to get the DNSDomainRecordRequestType from a string
func (p *Provider) StringToDNSDomainRecordRequestType(recordType string) (egoscale.CreateDNSDomainRecordRequestType, error) {
// Lookup the record type in the map
if recordType, exists := dnsRecordTypeMap[recordType]; exists {
return recordType, nil
}
return "", errors.New("invalid DNS record type")
}
// initClient will initialize the Exoscale API client with the provided api key and secret, and
// store the client in the Provider struct.
func (p *Provider) initClient() error {
if p.client == nil {
// Create new Exoscale client using the provided api_key and api_secret
// TODO: If exoscale wants the UserAgent to be set for Caddy, then we will need to set it as parameter of the Provider
client, err := egoscale.NewClient(
credentials.NewStaticCredentials(p.APIKey, p.APISecret),
egoscale.ClientOptWithUserAgent("libdns/exoscale"),
)
if err != nil {
return fmt.Errorf("exoscale: initializing client: %w", err)
}
p.client = client
}
return nil
}
// Internal helper function that actually creates the records
func (p *Provider) createRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
var createdRecords []libdns.Record
domain, err := p.findExistingZone(unFQDN(zone))
if err != nil {
return nil, fmt.Errorf("exoscale: %w", err)
}
if domain == nil {
return nil, fmt.Errorf("exoscale: zone %q not found", unFQDN(zone))
}
for _, r := range records {
recordType, err := p.StringToDNSDomainRecordRequestType(r.Type)
if err != nil {
return nil, fmt.Errorf("exoscale: error while creating DNS record %w", err)
}
recordRequest := egoscale.CreateDNSDomainRecordRequest{
Content: r.Value,
Name: r.Name,
Ttl: int64(r.TTL.Seconds()),
Priority: int64(r.Priority),
Type: recordType,
}
op, err := p.client.CreateDNSDomainRecord(ctx, domain.ID, recordRequest)
if err != nil {
return nil, fmt.Errorf("exoscale: error while creating DNS record: %w", err)
}
_, err = p.client.Wait(ctx, op, egoscale.OperationStateSuccess)
if err != nil {
return nil, fmt.Errorf("exoscale: error while waiting for DNS record creation: %w", err)
}
// We need to set the ID of the libdns.Record, so we need to query for the created Records
recordID, err := p.findExistingRecordID(domain.ID, r)
if err != nil {
return nil, fmt.Errorf("exoscale: error while finding DNS record %w", err)
}
if recordID == "" {
return nil, fmt.Errorf("exoscale: record not Found %q", r.Name)
}
r.ID = string(recordID)
createdRecords = append(createdRecords, r)
}
return createdRecords, nil
}
// findExistingZone Query Exoscale to find an existing zone for this name.
// Returns nil result if no zone could be found.
func (p *Provider) findExistingZone(zoneName string) (*egoscale.DNSDomain, error) {
ctx := context.Background()
dnsDomainResp, err := p.client.ListDNSDomains(ctx)
if err != nil {
return nil, fmt.Errorf("error while retrieving DNS zones: %w", err)
}
domain, err := dnsDomainResp.FindDNSDomain(zoneName)
if err != nil {
return nil, fmt.Errorf("error while retrieving DNS zones: %w", err)
}
return &domain, nil
}
// findExistingRecordID Query Exoscale to find an existing record for this name.
// Returns empty result if no record could be found.
func (p *Provider) findExistingRecordID(zoneID egoscale.UUID, record libdns.Record) (egoscale.UUID, error) {
ctx := context.Background()
records, err := p.client.ListDNSDomainRecords(ctx, zoneID)
if err != nil {
return "", fmt.Errorf("error while retrieving DNS records: %w", err)
}
for _, r := range records.DNSDomainRecords {
// we must unquote TXT records as we receive "\"123d==\"" when we expect "123d=="
content := strings.TrimRight(strings.TrimLeft(r.Content, "\""), "\"")
if r.Name == record.Name && string(r.Type) == record.Type && content == record.Value {
return r.ID, nil
}
}
return "", nil
}
func (p *Provider) getRecordsFromProvider(ctx context.Context, zone string) ([]libdns.Record, error) {
var records []libdns.Record
domain, err := p.findExistingZone(unFQDN(zone))
if err != nil {
return nil, fmt.Errorf("exoscale: %w", err)
}
if domain == nil {
return nil, fmt.Errorf("exoscale: zone %q not found", zone)
}
domainRecords, err := p.client.ListDNSDomainRecords(ctx, domain.ID)
if err != nil {
return nil, fmt.Errorf("exoscale: %w", err)
}
for _, r := range domainRecords.DNSDomainRecords {
record := libdns.Record{
ID: string(r.ID),
Type: string(r.Type),
Name: r.Name,
Value: r.Content,
TTL: time.Duration(r.Ttl),
Priority: uint(r.Priority),
}
records = append(records, record)
}
return records, nil
}
// Internal helper function to get the lists of records to create and update respectively
func (p *Provider) getRecordsToCreateAndUpdate(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, []libdns.Record, error) {
existingRecords, err := p.getRecordsFromProvider(ctx, zone)
if err != nil {
return nil, nil, err
}
var recordsToUpdate []libdns.Record
updateMap := make(map[libdns.Record]bool)
var recordsToCreate []libdns.Record
// Figure out which records exist and need to be updated
for _, r := range records {
updateMap[r] = true
for _, er := range existingRecords {
if r.Name != er.Name {
continue
}
if r.ID == "0" || r.ID == "" {
r.ID = er.ID
}
recordsToUpdate = append(recordsToUpdate, r)
updateMap[r] = false
}
}
// If the record is not updating an existing record, we want to create it
for r, updating := range updateMap {
if updating {
recordsToCreate = append(recordsToCreate, r)
}
}
return recordsToCreate, recordsToUpdate, nil
}
// GetRecords lists all the records in the zone.
func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
if err := p.initClient(); err != nil {
return nil, err
}
return p.getRecordsFromProvider(ctx, zone)
}
// AppendRecords adds records to the zone. It returns the records that were added.
func (p *Provider) AppendRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
if err := p.initClient(); err != nil {
return nil, err
}
return p.createRecords(ctx, zone, records)
}
// SetRecords sets the records in the zone, either by updating existing records or creating new ones.
// It returns the updated records.
func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
var setRecords []libdns.Record
if err := p.initClient(); err != nil {
return nil, err
}
recordsToCreate, recordsToUpdate, err := p.getRecordsToCreateAndUpdate(ctx, zone, records)
if err != nil {
return nil, err
}
// Create new records and append them to 'setRecords'
createdRecords, err := p.createRecords(ctx, zone, recordsToCreate)
if err != nil {
return nil, err
}
for _, r := range createdRecords {
setRecords = append(setRecords, r)
}
// Get Zone from zone name
domain, err := p.findExistingZone(unFQDN(zone))
if err != nil {
return nil, fmt.Errorf("exoscale: %w", err)
}
if domain == nil {
return nil, fmt.Errorf("exoscale: zone %q not found", unFQDN(zone))
}
for _, r := range recordsToUpdate {
recordRequest := egoscale.UpdateDNSDomainRecordRequest{
Content: r.Value,
Name: r.Name,
Ttl: int64(r.TTL.Seconds()),
Priority: int64(r.Priority),
}
op, err := p.client.UpdateDNSDomainRecord(ctx, domain.ID, egoscale.UUID(r.ID), recordRequest)
if err != nil {
return nil, fmt.Errorf("exoscale: error while updating DNS record: %w", err)
}
_, err = p.client.Wait(ctx, op, egoscale.OperationStateSuccess)
if err != nil {
return nil, fmt.Errorf("exoscale: error while waiting for DNS record update: %w", err)
}
setRecords = append(setRecords, r)
}
return setRecords, nil
}
// DeleteRecords deletes the records from the zone. It returns the records that were deleted.
func (p *Provider) DeleteRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
var deletedRecords []libdns.Record
if err := p.initClient(); err != nil {
return nil, err
}
domain, err := p.findExistingZone(unFQDN(zone))
if err != nil {
return nil, fmt.Errorf("exoscale: %w", err)
}
if domain == nil {
return nil, fmt.Errorf("exoscale: zone %q not found", unFQDN(zone))
}
for _, r := range records {
recordID, err := p.findExistingRecordID(domain.ID, r)
if err != nil {
return nil, fmt.Errorf("exoscale: error while finding DNS record %w", err)
}
if recordID == "" {
return nil, fmt.Errorf("exoscale: record not Found %q", r.Name)
}
op, err := p.client.DeleteDNSDomainRecord(ctx, domain.ID, recordID)
if err != nil {
return nil, fmt.Errorf("exoscale: error while deleting DNS record: %w", err)
}
_, err = p.client.Wait(ctx, op, egoscale.OperationStateSuccess)
if err != nil {
return nil, fmt.Errorf("exoscale: error while waiting DNS record deletion: %w", err)
}
deletedRecords = append(deletedRecords, r)
}
return deletedRecords, nil
}
// unFQDN trims any trailing "." from fqdn. Exoscale's API does not use FQDNs.
func unFQDN(fqdn string) string {
return strings.TrimSuffix(fqdn, ".")
}
// Interface guards
var (
_ libdns.RecordGetter = (*Provider)(nil)
_ libdns.RecordAppender = (*Provider)(nil)
_ libdns.RecordSetter = (*Provider)(nil)
_ libdns.RecordDeleter = (*Provider)(nil)
)