-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathprovider.go
216 lines (167 loc) · 6.39 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
// Package hosttech implements methods for manipulating Hosttech.ch DNS records with the libdns interfaces.
// Manipulation is achieved with the Hosttech API at https://api.ns1.hosttech.eu/api/documentation/#/.
package hosttech
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/libdns/libdns"
)
// Provider facilitates DNS record manipulation with Hosttech.ch.
type Provider struct {
APIToken string `json:"api_token,omitempty"`
}
// The URL for the Hosttech API connection
const apiHost = "https://api.ns1.hosttech.eu/api/user/v1"
// GetRecords lists all the records in the zone.
func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
reqURL := fmt.Sprintf("%s/zones/%s/records", apiHost, RemoveTrailingDot(zone))
responseBody, err := p.makeApiCall(ctx, http.MethodGet, reqURL, nil)
//If there's an error return an empty slice
if err != nil {
return []libdns.Record{}, err
}
var parsedResponse = HosttechListResponseWrapper{}
err = json.Unmarshal(responseBody, &parsedResponse)
if err != nil {
return []libdns.Record{}, err
}
var libdnsRecords []libdns.Record
for _, record := range parsedResponse.Data {
libdnsRecords = append(libdnsRecords, record.toLibdnsRecord(zone))
}
return libdnsRecords, nil
}
// AppendRecords adds records to the zone. It returns all records that were added.
// If an error occurs while records are being added, the already successfully added records will be returned along with an error.
func (p *Provider) AppendRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
reqURL := fmt.Sprintf("%s/zones/%s/records", apiHost, RemoveTrailingDot(zone))
successfullyAppendedRecords := []libdns.Record{}
for _, record := range records {
hosttechRecord, err := LibdnsRecordToHosttechRecordWrapper(record)
if err != nil {
return nil, err
}
bodyBytes, err := json.Marshal(hosttechRecord)
if err != nil {
return nil, err
}
responseBody, err := p.makeApiCall(ctx, http.MethodPost, reqURL, bytes.NewReader(bodyBytes))
if err != nil {
return successfullyAppendedRecords, err
}
var parsedResponse = HosttechSingleResponseWrapper{}
err = json.Unmarshal(responseBody, &parsedResponse)
if err != nil {
return []libdns.Record{}, err
}
successfullyAppendedRecords = append(successfullyAppendedRecords, parsedResponse.Data.toLibdnsRecord(zone))
}
return successfullyAppendedRecords, nil
}
// 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) {
successfullyUpdatedRecords := []libdns.Record{}
for _, record := range records {
hosttechRecord, err := LibdnsRecordToHosttechRecordWrapper(record)
if err != nil {
return nil, err
}
bodyBytes, err := json.Marshal(hosttechRecord)
if err != nil {
return nil, err
}
reqURL := fmt.Sprintf("%s/zones/%s/records/%s", apiHost, RemoveTrailingDot(zone), record.ID)
responseBody, err := p.makeApiCall(ctx, http.MethodPut, reqURL, bytes.NewReader(bodyBytes))
if err != nil {
//If the error doesn't have anything to do with the api, return
apiError, ok := err.(ApiError)
if !ok {
return successfullyUpdatedRecords, err
}
//If the error isn't a 404, return
if apiError.ErrorCode != 404 {
return successfullyUpdatedRecords, err
}
//If the error was a 404, the record could not be updated because it didn't exist. So we create a new one
appendedRecords, err := p.AppendRecords(ctx, zone, []libdns.Record{record})
if err != nil {
return successfullyUpdatedRecords, err
}
successfullyUpdatedRecords = append(successfullyUpdatedRecords, appendedRecords...)
continue
}
var parsedResponse = HosttechSingleResponseWrapper{}
err = json.Unmarshal(responseBody, &parsedResponse)
if err != nil {
return []libdns.Record{}, err
}
successfullyUpdatedRecords = append(successfullyUpdatedRecords, parsedResponse.Data.toLibdnsRecord(zone))
}
return successfullyUpdatedRecords, nil
}
// DeleteRecords deletes the records from the zone. It returns the records that were deleted.
// If an error occurs while records are being deleted, the already successfully deleted records will be returned along with an error.
func (p *Provider) DeleteRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
successfullyDeletedRecords := []libdns.Record{}
for _, record := range records {
reqUrl := fmt.Sprintf("%s/zones/%s/records/%s", apiHost, RemoveTrailingDot(zone), record.ID)
_, err := p.makeApiCall(ctx, http.MethodDelete, reqUrl, nil)
if err != nil {
return successfullyDeletedRecords, err
}
successfullyDeletedRecords = append(successfullyDeletedRecords, record)
}
return successfullyDeletedRecords, nil
}
// List all available zones
func (p *Provider) ListZones(ctx context.Context) ([]libdns.Zone, error) {
reqUrl := fmt.Sprintf("%s/zones", apiHost)
responseBody, err := p.makeApiCall(ctx, http.MethodGet, reqUrl, nil)
if err != nil {
return nil, err
}
var parsedResponse = HosttechZoneListResponseWrapper{}
err = json.Unmarshal(responseBody, &parsedResponse)
if err != nil {
return nil, err
}
var libdnsZones []libdns.Zone
for _, zone := range parsedResponse.Data {
libdnsZones = append(libdnsZones, zone.toLibdnsZone())
}
return libdnsZones, nil
}
func (p *Provider) makeApiCall(ctx context.Context, httpMethod string, reqUrl string, body io.Reader) (response []byte, err error) {
req, err := http.NewRequestWithContext(ctx, httpMethod, reqUrl, body)
req.Header.Set("Authorization", "Bearer "+p.APIToken)
req.Header.Set("Content-Type", "application/json")
//Return nil if there's an error
if err != nil {
return
}
resp, err := http.DefaultClient.Do(req)
//Return an empty slice if there's an error
if err != nil {
return
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, ApiError{
s: fmt.Sprintf("call to API was not successful, returned the status code '%s'", resp.Status),
ErrorCode: resp.StatusCode,
}
}
return io.ReadAll(resp.Body)
}
// Interface guards
var (
_ libdns.RecordGetter = (*Provider)(nil)
_ libdns.RecordAppender = (*Provider)(nil)
_ libdns.RecordSetter = (*Provider)(nil)
_ libdns.RecordDeleter = (*Provider)(nil)
_ libdns.ZoneLister = (*Provider)(nil)
)