0 | |
From 2766aa6987ceb4505d1e3a6adf257df6cd90e436 Mon Sep 17 00:00:00 2001
|
1 | |
From: Iku Iwasa <iku.iwasa@gmail.com>
|
2 | |
Date: Sat, 28 Nov 2020 22:53:02 +0900
|
3 | |
Subject: vultr: Update to govultr v2 API (#1302)
|
4 | |
|
5 | |
Origin: upstream, https://github.com/go-acme/lego/commit/2766aa6987ceb4505d1e3a6adf257df6cd90e436
|
6 | |
---
|
7 | |
go.mod | 2 +-
|
8 | |
go.sum | 8 +-
|
9 | |
providers/dns/vultr/vultr.go | 99 +++++++++++++++--------
|
10 | |
providers/dns/vultr/vultr_test.go | 129 ++++++++++++++++++++++++++++++
|
11 | |
4 files changed, 201 insertions(+), 37 deletions(-)
|
12 | |
|
13 | |
diff --git a/go.mod b/go.mod
|
14 | |
index ff74db1b..0d80c679 100644
|
15 | |
--- a/go.mod
|
16 | |
+++ b/go.mod
|
17 | |
@@ -42,7 +42,7 @@ require (
|
18 | |
github.com/stretchr/testify v1.6.1
|
19 | |
github.com/transip/gotransip/v6 v6.2.0
|
20 | |
github.com/urfave/cli v1.22.4
|
21 | |
- github.com/vultr/govultr v0.5.0
|
22 | |
+ github.com/vultr/govultr/v2 v2.0.0
|
23 | |
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
24 | |
golang.org/x/net v0.0.0-20200822124328-c89045814202
|
25 | |
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
26 | |
diff --git a/go.sum b/go.sum
|
27 | |
index f80bfd87..370c4fde 100644
|
28 | |
--- a/go.sum
|
29 | |
+++ b/go.sum
|
30 | |
@@ -191,8 +191,8 @@ github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVo
|
31 | |
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
32 | |
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
33 | |
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
34 | |
-github.com/hashicorp/go-retryablehttp v0.6.7 h1:8/CAEZt/+F7kR7GevNHulKkUjLht3CPmn7egmhieNKo=
|
35 | |
-github.com/hashicorp/go-retryablehttp v0.6.7/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
36 | |
+github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM=
|
37 | |
+github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
38 | |
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
39 | |
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
40 | |
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
41 | |
@@ -341,8 +341,8 @@ github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo=
|
42 | |
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
43 | |
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
|
44 | |
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
45 | |
-github.com/vultr/govultr v0.5.0 h1:iQzYhzbokmpDARbvIkvTkoyS7WMH82zVTKAL1PZ4JOA=
|
46 | |
-github.com/vultr/govultr v0.5.0/go.mod h1:wZZXZbYbqyY1n3AldoeYNZK4Wnmmoq6dNFkvd5TV3ss=
|
47 | |
+github.com/vultr/govultr/v2 v2.0.0 h1:+lAtqfWy3g9VwL7tT2Fpyad8Vv4MxOhT/NU8O5dk+EQ=
|
48 | |
+github.com/vultr/govultr/v2 v2.0.0/go.mod h1:2PsEeg+gs3p/Fo5Pw8F9mv+DUBEOlrNZ8GmCTGmhOhs=
|
49 | |
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
50 | |
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
51 | |
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
52 | |
diff --git a/providers/dns/vultr/vultr.go b/providers/dns/vultr/vultr.go
|
53 | |
index e2f6cc3a..4ea20dc7 100644
|
54 | |
--- a/providers/dns/vultr/vultr.go
|
55 | |
+++ b/providers/dns/vultr/vultr.go
|
56 | |
@@ -4,17 +4,16 @@ package vultr
|
57 | |
|
58 | |
import (
|
59 | |
"context"
|
60 | |
- "crypto/tls"
|
61 | |
"errors"
|
62 | |
"fmt"
|
63 | |
"net/http"
|
64 | |
- "strconv"
|
65 | |
"strings"
|
66 | |
"time"
|
67 | |
|
68 | |
"github.com/go-acme/lego/v4/challenge/dns01"
|
69 | |
"github.com/go-acme/lego/v4/platform/config/env"
|
70 | |
- "github.com/vultr/govultr"
|
71 | |
+ "github.com/vultr/govultr/v2"
|
72 | |
+ "golang.org/x/oauth2"
|
73 | |
)
|
74 | |
|
75 | |
// Environment variables names.
|
76 | |
@@ -36,6 +35,7 @@ type Config struct {
|
77 | |
PollingInterval time.Duration
|
78 | |
TTL int
|
79 | |
HTTPClient *http.Client
|
80 | |
+ HTTPTimeout time.Duration
|
81 | |
}
|
82 | |
|
83 | |
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
84 | |
@@ -44,13 +44,7 @@ func NewDefaultConfig() *Config {
|
85 | |
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
|
86 | |
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
|
87 | |
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
88 | |
- HTTPClient: &http.Client{
|
89 | |
- Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30),
|
90 | |
- // from Vultr Client
|
91 | |
- Transport: &http.Transport{
|
92 | |
- TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper),
|
93 | |
- },
|
94 | |
- },
|
95 | |
+ HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30),
|
96 | |
}
|
97 | |
}
|
98 | |
|
99 | |
@@ -84,7 +78,17 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
100 | |
return nil, errors.New("vultr: credentials missing")
|
101 | |
}
|
102 | |
|
103 | |
- client := govultr.NewClient(config.HTTPClient, config.APIKey)
|
104 | |
+ httpClient := config.HTTPClient
|
105 | |
+ if httpClient == nil {
|
106 | |
+ httpClient = &http.Client{
|
107 | |
+ Timeout: config.HTTPTimeout,
|
108 | |
+ Transport: &oauth2.Transport{
|
109 | |
+ Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.APIKey}),
|
110 | |
+ },
|
111 | |
+ }
|
112 | |
+ }
|
113 | |
+
|
114 | |
+ client := govultr.NewClient(httpClient)
|
115 | |
|
116 | |
return &DNSProvider{client: client, config: config}, nil
|
117 | |
}
|
118 | |
@@ -102,7 +106,14 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
119 | |
|
120 | |
name := extractRecordName(fqdn, zoneDomain)
|
121 | |
|
122 | |
- err = d.client.DNSRecord.Create(ctx, zoneDomain, "TXT", name, `"`+value+`"`, d.config.TTL, 0)
|
123 | |
+ req := govultr.DomainRecordReq{
|
124 | |
+ Name: name,
|
125 | |
+ Type: "TXT",
|
126 | |
+ Data: `"` + value + `"`,
|
127 | |
+ TTL: d.config.TTL,
|
128 | |
+ Priority: func(v int) *int { return &v }(0),
|
129 | |
+ }
|
130 | |
+ _, err = d.client.DomainRecord.Create(ctx, zoneDomain, &req)
|
131 | |
if err != nil {
|
132 | |
return fmt.Errorf("vultr: API call failed: %w", err)
|
133 | |
}
|
134 | |
@@ -123,7 +134,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
135 | |
|
136 | |
var allErr []string
|
137 | |
for _, rec := range records {
|
138 | |
- err := d.client.DNSRecord.Delete(ctx, zoneDomain, strconv.Itoa(rec.RecordID))
|
139 | |
+ err := d.client.DomainRecord.Delete(ctx, zoneDomain, rec.ID)
|
140 | |
if err != nil {
|
141 | |
allErr = append(allErr, err.Error())
|
142 | |
}
|
143 | |
@@ -143,43 +154,67 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
144 | |
}
|
145 | |
|
146 | |
func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (string, error) {
|
147 | |
- domains, err := d.client.DNSDomain.List(ctx)
|
148 | |
- if err != nil {
|
149 | |
- return "", fmt.Errorf("API call failed: %w", err)
|
150 | |
- }
|
151 | |
+ listOptions := &govultr.ListOptions{PerPage: 25}
|
152 | |
+
|
153 | |
+ var hostedDomain govultr.Domain
|
154 | |
|
155 | |
- var hostedDomain govultr.DNSDomain
|
156 | |
- for _, dom := range domains {
|
157 | |
- if strings.HasSuffix(domain, dom.Domain) {
|
158 | |
- if len(dom.Domain) > len(hostedDomain.Domain) {
|
159 | |
+ for {
|
160 | |
+ domains, meta, err := d.client.Domain.List(ctx, listOptions)
|
161 | |
+ if err != nil {
|
162 | |
+ return "", fmt.Errorf("API call failed: %w", err)
|
163 | |
+ }
|
164 | |
+
|
165 | |
+ for _, dom := range domains {
|
166 | |
+ if strings.HasSuffix(domain, dom.Domain) && len(dom.Domain) > len(hostedDomain.Domain) {
|
167 | |
hostedDomain = dom
|
168 | |
}
|
169 | |
}
|
170 | |
+
|
171 | |
+ if domain == hostedDomain.Domain {
|
172 | |
+ break
|
173 | |
+ }
|
174 | |
+
|
175 | |
+ if meta.Links.Next == "" {
|
176 | |
+ break
|
177 | |
+ }
|
178 | |
+
|
179 | |
+ listOptions.Cursor = meta.Links.Next
|
180 | |
}
|
181 | |
+
|
182 | |
if hostedDomain.Domain == "" {
|
183 | |
- return "", fmt.Errorf("no matching Vultr domain found for domain %s", domain)
|
184 | |
+ return "", fmt.Errorf("no matching domain found for domain %s", domain)
|
185 | |
}
|
186 | |
|
187 | |
return hostedDomain.Domain, nil
|
188 | |
}
|
189 | |
|
190 | |
-func (d *DNSProvider) findTxtRecords(ctx context.Context, domain, fqdn string) (string, []govultr.DNSRecord, error) {
|
191 | |
+func (d *DNSProvider) findTxtRecords(ctx context.Context, domain, fqdn string) (string, []govultr.DomainRecord, error) {
|
192 | |
zoneDomain, err := d.getHostedZone(ctx, domain)
|
193 | |
if err != nil {
|
194 | |
return "", nil, err
|
195 | |
}
|
196 | |
|
197 | |
- var records []govultr.DNSRecord
|
198 | |
- result, err := d.client.DNSRecord.List(ctx, zoneDomain)
|
199 | |
- if err != nil {
|
200 | |
- return "", records, fmt.Errorf("API call has failed: %w", err)
|
201 | |
- }
|
202 | |
+ listOptions := &govultr.ListOptions{PerPage: 25}
|
203 | |
|
204 | |
- recordName := extractRecordName(fqdn, zoneDomain)
|
205 | |
- for _, record := range result {
|
206 | |
- if record.Type == "TXT" && record.Name == recordName {
|
207 | |
- records = append(records, record)
|
208 | |
+ var records []govultr.DomainRecord
|
209 | |
+ for {
|
210 | |
+ result, meta, err := d.client.DomainRecord.List(ctx, zoneDomain, listOptions)
|
211 | |
+ if err != nil {
|
212 | |
+ return "", records, fmt.Errorf("API call has failed: %w", err)
|
213 | |
}
|
214 | |
+
|
215 | |
+ recordName := extractRecordName(fqdn, zoneDomain)
|
216 | |
+ for _, record := range result {
|
217 | |
+ if record.Type == "TXT" && record.Name == recordName {
|
218 | |
+ records = append(records, record)
|
219 | |
+ }
|
220 | |
+ }
|
221 | |
+
|
222 | |
+ if meta.Links.Next == "" {
|
223 | |
+ break
|
224 | |
+ }
|
225 | |
+
|
226 | |
+ listOptions.Cursor = meta.Links.Next
|
227 | |
}
|
228 | |
|
229 | |
return zoneDomain, records, nil
|
230 | |
diff --git a/providers/dns/vultr/vultr_test.go b/providers/dns/vultr/vultr_test.go
|
231 | |
index 0144758e..75d64883 100644
|
232 | |
--- a/providers/dns/vultr/vultr_test.go
|
233 | |
+++ b/providers/dns/vultr/vultr_test.go
|
234 | |
@@ -1,11 +1,19 @@
|
235 | |
package vultr
|
236 | |
|
237 | |
import (
|
238 | |
+ "context"
|
239 | |
+ "encoding/json"
|
240 | |
+ "fmt"
|
241 | |
+ "net/http"
|
242 | |
+ "net/http/httptest"
|
243 | |
+ "strconv"
|
244 | |
"testing"
|
245 | |
"time"
|
246 | |
|
247 | |
"github.com/go-acme/lego/v4/platform/tester"
|
248 | |
+ "github.com/stretchr/testify/assert"
|
249 | |
"github.com/stretchr/testify/require"
|
250 | |
+ "github.com/vultr/govultr/v2"
|
251 | |
)
|
252 | |
|
253 | |
const envDomain = envNamespace + "TEST_DOMAIN"
|
254 | |
@@ -90,6 +98,127 @@ func TestNewDNSProviderConfig(t *testing.T) {
|
255 | |
}
|
256 | |
}
|
257 | |
|
258 | |
+func TestDNSProvider_getHostedZone(t *testing.T) {
|
259 | |
+ testCases := []struct {
|
260 | |
+ desc string
|
261 | |
+ domain string
|
262 | |
+ expected string
|
263 | |
+ expectedPageCount int
|
264 | |
+ }{
|
265 | |
+ {
|
266 | |
+ desc: "exact match, in latest page",
|
267 | |
+ domain: "test.my.example.com",
|
268 | |
+ expected: "test.my.example.com",
|
269 | |
+ expectedPageCount: 5,
|
270 | |
+ },
|
271 | |
+ {
|
272 | |
+ desc: "exact match, in the middle",
|
273 | |
+ domain: "my.example.com",
|
274 | |
+ expected: "my.example.com",
|
275 | |
+ expectedPageCount: 3,
|
276 | |
+ },
|
277 | |
+ {
|
278 | |
+ desc: "exact match, first page",
|
279 | |
+ domain: "example.com",
|
280 | |
+ expected: "example.com",
|
281 | |
+ expectedPageCount: 1,
|
282 | |
+ },
|
283 | |
+ {
|
284 | |
+ desc: "match on apex",
|
285 | |
+ domain: "test.example.org",
|
286 | |
+ expected: "example.org",
|
287 | |
+ expectedPageCount: 5,
|
288 | |
+ },
|
289 | |
+ {
|
290 | |
+ desc: "match on parent",
|
291 | |
+ domain: "test.my.example.net",
|
292 | |
+ expected: "my.example.net",
|
293 | |
+ expectedPageCount: 5,
|
294 | |
+ },
|
295 | |
+ }
|
296 | |
+
|
297 | |
+ domains := []govultr.Domain{{Domain: "example.com"}, {Domain: "example.org"}, {Domain: "example.net"}}
|
298 | |
+
|
299 | |
+ for i := 0; i < 50; i++ {
|
300 | |
+ domains = append(domains, govultr.Domain{Domain: fmt.Sprintf("my%02d.example.com", i)})
|
301 | |
+ }
|
302 | |
+
|
303 | |
+ domains = append(domains, govultr.Domain{Domain: "my.example.com"}, govultr.Domain{Domain: "my.example.net"})
|
304 | |
+
|
305 | |
+ for i := 50; i < 100; i++ {
|
306 | |
+ domains = append(domains, govultr.Domain{Domain: fmt.Sprintf("my%02d.example.com", i)})
|
307 | |
+ }
|
308 | |
+
|
309 | |
+ domains = append(domains, govultr.Domain{Domain: "test.my.example.com"})
|
310 | |
+
|
311 | |
+ type domainsBase struct {
|
312 | |
+ Domains []govultr.Domain `json:"domains"`
|
313 | |
+ Meta *govultr.Meta `json:"meta"`
|
314 | |
+ }
|
315 | |
+
|
316 | |
+ for _, test := range testCases {
|
317 | |
+ test := test
|
318 | |
+ t.Run(test.desc, func(t *testing.T) {
|
319 | |
+ t.Parallel()
|
320 | |
+
|
321 | |
+ mux := http.NewServeMux()
|
322 | |
+ server := httptest.NewServer(mux)
|
323 | |
+ t.Cleanup(server.Close)
|
324 | |
+
|
325 | |
+ client := govultr.NewClient(nil)
|
326 | |
+ err := client.SetBaseURL(server.URL)
|
327 | |
+ require.NoError(t, err)
|
328 | |
+
|
329 | |
+ p := &DNSProvider{client: client}
|
330 | |
+
|
331 | |
+ var pageCount int
|
332 | |
+
|
333 | |
+ mux.HandleFunc("/v2/domains", func(rw http.ResponseWriter, req *http.Request) {
|
334 | |
+ pageCount++
|
335 | |
+
|
336 | |
+ query := req.URL.Query()
|
337 | |
+ cursor, _ := strconv.Atoi(query.Get("cursor"))
|
338 | |
+ perPage, _ := strconv.Atoi(query.Get("per_page"))
|
339 | |
+
|
340 | |
+ var next string
|
341 | |
+ if len(domains)/perPage > cursor {
|
342 | |
+ next = strconv.Itoa(cursor + 1)
|
343 | |
+ }
|
344 | |
+
|
345 | |
+ start := cursor * perPage
|
346 | |
+ if len(domains) < start {
|
347 | |
+ start = cursor * len(domains)
|
348 | |
+ }
|
349 | |
+
|
350 | |
+ end := (cursor + 1) * perPage
|
351 | |
+ if len(domains) < end {
|
352 | |
+ end = len(domains)
|
353 | |
+ }
|
354 | |
+
|
355 | |
+ db := domainsBase{
|
356 | |
+ Domains: domains[start:end],
|
357 | |
+ Meta: &govultr.Meta{
|
358 | |
+ Total: len(domains),
|
359 | |
+ Links: &govultr.Links{Next: next},
|
360 | |
+ },
|
361 | |
+ }
|
362 | |
+
|
363 | |
+ err = json.NewEncoder(rw).Encode(db)
|
364 | |
+ if err != nil {
|
365 | |
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
|
366 | |
+ return
|
367 | |
+ }
|
368 | |
+ })
|
369 | |
+
|
370 | |
+ zone, err := p.getHostedZone(context.Background(), test.domain)
|
371 | |
+ require.NoError(t, err)
|
372 | |
+
|
373 | |
+ assert.Equal(t, test.expected, zone)
|
374 | |
+ assert.Equal(t, test.expectedPageCount, pageCount)
|
375 | |
+ })
|
376 | |
+ }
|
377 | |
+}
|
378 | |
+
|
379 | |
func TestLivePresent(t *testing.T) {
|
380 | |
if !envTest.IsLiveTest() {
|
381 | |
t.Skip("skipping live test")
|
382 | |
--
|
383 | |
2.37.2
|
384 | |
|