forked from TrueCloudLab/lego
scaleway: use official SDK (#1393)
This commit is contained in:
parent
8bf9613fff
commit
6bd3eae58f
9 changed files with 72 additions and 461 deletions
|
@ -1627,12 +1627,10 @@ func displayDNSHelp(name string) error {
|
||||||
|
|
||||||
ew.writeln(`Credentials:`)
|
ew.writeln(`Credentials:`)
|
||||||
ew.writeln(` - "SCALEWAY_API_TOKEN": API token`)
|
ew.writeln(` - "SCALEWAY_API_TOKEN": API token`)
|
||||||
|
ew.writeln(` - "SCALEWAY_PROJECT_ID": Project to use (optional)`)
|
||||||
ew.writeln()
|
ew.writeln()
|
||||||
|
|
||||||
ew.writeln(`Additional Configuration:`)
|
ew.writeln(`Additional Configuration:`)
|
||||||
ew.writeln(` - "SCALEWAY_API_VERSION": API version`)
|
|
||||||
ew.writeln(` - "SCALEWAY_BASE_URL": API endpoint URL`)
|
|
||||||
ew.writeln(` - "SCALEWAY_HTTP_TIMEOUT": API request timeout`)
|
|
||||||
ew.writeln(` - "SCALEWAY_POLLING_INTERVAL": Time between DNS propagation check`)
|
ew.writeln(` - "SCALEWAY_POLLING_INTERVAL": Time between DNS propagation check`)
|
||||||
ew.writeln(` - "SCALEWAY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
|
ew.writeln(` - "SCALEWAY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
|
||||||
ew.writeln(` - "SCALEWAY_TTL": The TTL of the TXT record used for the DNS challenge`)
|
ew.writeln(` - "SCALEWAY_TTL": The TTL of the TXT record used for the DNS challenge`)
|
||||||
|
|
|
@ -33,6 +33,7 @@ lego --email myemail@example.com --dns scaleway --domains my.example.org run
|
||||||
| Environment Variable Name | Description |
|
| Environment Variable Name | Description |
|
||||||
|-----------------------|-------------|
|
|-----------------------|-------------|
|
||||||
| `SCALEWAY_API_TOKEN` | API token |
|
| `SCALEWAY_API_TOKEN` | API token |
|
||||||
|
| `SCALEWAY_PROJECT_ID` | Project to use (optional) |
|
||||||
|
|
||||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||||
More information [here](/lego/dns/#configuration-and-credentials).
|
More information [here](/lego/dns/#configuration-and-credentials).
|
||||||
|
@ -42,9 +43,6 @@ More information [here](/lego/dns/#configuration-and-credentials).
|
||||||
|
|
||||||
| Environment Variable Name | Description |
|
| Environment Variable Name | Description |
|
||||||
|--------------------------------|-------------|
|
|--------------------------------|-------------|
|
||||||
| `SCALEWAY_API_VERSION` | API version |
|
|
||||||
| `SCALEWAY_BASE_URL` | API endpoint URL |
|
|
||||||
| `SCALEWAY_HTTP_TIMEOUT` | API request timeout |
|
|
||||||
| `SCALEWAY_POLLING_INTERVAL` | Time between DNS propagation check |
|
| `SCALEWAY_POLLING_INTERVAL` | Time between DNS propagation check |
|
||||||
| `SCALEWAY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
|
| `SCALEWAY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
|
||||||
| `SCALEWAY_TTL` | The TTL of the TXT record used for the DNS challenge |
|
| `SCALEWAY_TTL` | The TTL of the TXT record used for the DNS challenge |
|
||||||
|
@ -57,7 +55,7 @@ More information [here](/lego/dns/#configuration-and-credentials).
|
||||||
|
|
||||||
## More information
|
## More information
|
||||||
|
|
||||||
- [API documentation](https://developers.scaleway.com/en/products/domain/api/)
|
- [API documentation](https://developers.scaleway.com/en/products/domain/dns/api/)
|
||||||
|
|
||||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||||
<!-- providers/dns/scaleway/scaleway.toml -->
|
<!-- providers/dns/scaleway/scaleway.toml -->
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -41,6 +41,7 @@ require (
|
||||||
github.com/pquerna/otp v1.3.0
|
github.com/pquerna/otp v1.3.0
|
||||||
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2
|
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2
|
||||||
github.com/sacloud/libsacloud v1.36.2
|
github.com/sacloud/libsacloud v1.36.2
|
||||||
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/transip/gotransip/v6 v6.2.0
|
github.com/transip/gotransip/v6 v6.2.0
|
||||||
github.com/urfave/cli v1.22.4
|
github.com/urfave/cli v1.22.4
|
||||||
|
|
3
go.sum
3
go.sum
|
@ -110,6 +110,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
|
github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
|
||||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||||
|
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||||
github.com/dnsimple/dnsimple-go v0.63.0 h1:0doY8VW/ckRIMTmOw4E1vwqo+bhtjDzvh1pU2ZteFGA=
|
github.com/dnsimple/dnsimple-go v0.63.0 h1:0doY8VW/ckRIMTmOw4E1vwqo+bhtjDzvh1pU2ZteFGA=
|
||||||
github.com/dnsimple/dnsimple-go v0.63.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
|
github.com/dnsimple/dnsimple-go v0.63.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
|
||||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||||
|
@ -399,6 +400,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
github.com/sacloud/libsacloud v1.36.2 h1:aosI7clbQ9IU0Hj+3rpk3SKJop5nLPpLThnWCivPqjI=
|
github.com/sacloud/libsacloud v1.36.2 h1:aosI7clbQ9IU0Hj+3rpk3SKJop5nLPpLThnWCivPqjI=
|
||||||
github.com/sacloud/libsacloud v1.36.2/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg=
|
github.com/sacloud/libsacloud v1.36.2/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg=
|
||||||
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f h1:WSnaD0/cvbKJgSTYbjAPf4RJXVvNNDAwVm+W8wEmnGE=
|
||||||
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
|
|
@ -1,228 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultEndpoint = "https://api.scaleway.com/domain/v2alpha2"
|
|
||||||
uriUpdateRecords = "/dns-zones/%s/records"
|
|
||||||
operationSet = "set"
|
|
||||||
operationDelete = "delete"
|
|
||||||
operationAdd = "add"
|
|
||||||
)
|
|
||||||
|
|
||||||
// APIError represents an error response from the API.
|
|
||||||
type APIError struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a APIError) Error() string {
|
|
||||||
return a.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record represents a DNS record.
|
|
||||||
type Record struct {
|
|
||||||
Data string `json:"data,omitempty"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Priority uint32 `json:"priority,omitempty"`
|
|
||||||
TTL uint32 `json:"ttl,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecordChangeAdd represents a list of add operations.
|
|
||||||
type RecordChangeAdd struct {
|
|
||||||
Records []*Record `json:"records,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecordChangeSet represents a list of set operations.
|
|
||||||
type RecordChangeSet struct {
|
|
||||||
Data string `json:"data,omitempty"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
TTL uint32 `json:"ttl,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Records []*Record `json:"records,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecordChangeDelete represents a list of delete operations.
|
|
||||||
type RecordChangeDelete struct {
|
|
||||||
Data string `json:"data,omitempty"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateDNSZoneRecordsRequest represents a request to update DNS records on the API.
|
|
||||||
type UpdateDNSZoneRecordsRequest struct {
|
|
||||||
DNSZone string `json:"dns_zone,omitempty"`
|
|
||||||
Changes []interface{} `json:"changes,omitempty"`
|
|
||||||
ReturnAllRecords bool `json:"return_all_records,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientOpts represents options to init client.
|
|
||||||
type ClientOpts struct {
|
|
||||||
BaseURL string
|
|
||||||
Token string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client represents DNS client.
|
|
||||||
type Client struct {
|
|
||||||
baseURL string
|
|
||||||
token string
|
|
||||||
httpClient *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient returns a client instance.
|
|
||||||
func NewClient(opts ClientOpts, httpClient *http.Client) *Client {
|
|
||||||
baseURL := defaultEndpoint
|
|
||||||
if opts.BaseURL != "" {
|
|
||||||
baseURL = opts.BaseURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if httpClient == nil {
|
|
||||||
httpClient = &http.Client{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Client{
|
|
||||||
token: opts.Token,
|
|
||||||
baseURL: baseURL,
|
|
||||||
httpClient: httpClient,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRecord adds Record for given zone.
|
|
||||||
func (c *Client) AddRecord(zone string, record Record) error {
|
|
||||||
changes := map[string]RecordChangeAdd{
|
|
||||||
operationAdd: {
|
|
||||||
Records: []*Record{&record},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
request := UpdateDNSZoneRecordsRequest{
|
|
||||||
DNSZone: zone,
|
|
||||||
Changes: []interface{}{changes},
|
|
||||||
ReturnAllRecords: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
uri := fmt.Sprintf(uriUpdateRecords, zone)
|
|
||||||
req, err := c.newRequest(http.MethodPatch, uri, request)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.do(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRecord sets a unique Record for given zone.
|
|
||||||
func (c *Client) SetRecord(zone string, record Record) error {
|
|
||||||
changes := map[string]RecordChangeSet{
|
|
||||||
operationSet: {
|
|
||||||
Name: record.Name,
|
|
||||||
Type: record.Type,
|
|
||||||
Records: []*Record{&record},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
request := UpdateDNSZoneRecordsRequest{
|
|
||||||
DNSZone: zone,
|
|
||||||
Changes: []interface{}{changes},
|
|
||||||
ReturnAllRecords: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
uri := fmt.Sprintf(uriUpdateRecords, zone)
|
|
||||||
req, err := c.newRequest(http.MethodPatch, uri, request)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.do(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRecord deletes a Record for given zone.
|
|
||||||
func (c *Client) DeleteRecord(zone string, record Record) error {
|
|
||||||
delRecord := map[string]RecordChangeDelete{
|
|
||||||
operationDelete: {
|
|
||||||
Name: record.Name,
|
|
||||||
Type: record.Type,
|
|
||||||
Data: record.Data,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
request := UpdateDNSZoneRecordsRequest{
|
|
||||||
DNSZone: zone,
|
|
||||||
Changes: []interface{}{delRecord},
|
|
||||||
ReturnAllRecords: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
uri := fmt.Sprintf(uriUpdateRecords, zone)
|
|
||||||
req, err := c.newRequest(http.MethodPatch, uri, request)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.do(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newRequest(method, uri string, body interface{}) (*http.Request, error) {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
|
|
||||||
if body != nil {
|
|
||||||
err := json.NewEncoder(buf).Encode(body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to encode request body with error: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(method, c.baseURL+uri, buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create new http request with error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("X-auth-token", c.token)
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) do(req *http.Request) error {
|
|
||||||
resp, err := c.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("request failed with error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = checkResponse(resp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkResponse(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkResponse(resp *http.Response) error {
|
|
||||||
if resp.StatusCode >= http.StatusBadRequest || resp.StatusCode < http.StatusOK {
|
|
||||||
if resp.Body == nil {
|
|
||||||
return fmt.Errorf("request failed with status code %d and empty body", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
apiError := APIError{}
|
|
||||||
err = json.Unmarshal(body, &apiError)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("request failed with status code %d, response body: %s", resp.StatusCode, string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("request failed with status code %d: %w", resp.StatusCode, apiError)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,176 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
const fakeToken = "test"
|
|
||||||
|
|
||||||
func setupTest() (*Client, *http.ServeMux, func()) {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
svr := httptest.NewServer(mux)
|
|
||||||
|
|
||||||
opts := ClientOpts{
|
|
||||||
BaseURL: svr.URL,
|
|
||||||
Token: fakeToken,
|
|
||||||
}
|
|
||||||
client := NewClient(opts, nil)
|
|
||||||
|
|
||||||
return client, mux, func() {
|
|
||||||
svr.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_AddRecord(t *testing.T) {
|
|
||||||
client, mux, tearDown := setupTest()
|
|
||||||
defer tearDown()
|
|
||||||
|
|
||||||
mux.HandleFunc("/dns-zones/zone/records", func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
if req.Method != http.MethodPatch {
|
|
||||||
http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
auth := req.Header.Get("X-Auth-Token")
|
|
||||||
if auth != fakeToken {
|
|
||||||
http.Error(rw, fmt.Sprintf("invalid token: %s", auth), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := ioutil.ReadAll(req.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := `{"dns_zone":"zone","changes":[{"add":{"records":[{"data":"\"value\"","name":"fqdn","ttl":30,"type":"TXT"}]}}]}`
|
|
||||||
assert.Equal(t, expected+"\n", string(raw))
|
|
||||||
})
|
|
||||||
|
|
||||||
record := Record{
|
|
||||||
Type: "TXT",
|
|
||||||
TTL: 30,
|
|
||||||
Name: "fqdn",
|
|
||||||
Data: fmt.Sprintf(`"%s"`, "value"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := client.AddRecord("zone", record)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_AddRecord_error(t *testing.T) {
|
|
||||||
client, mux, tearDown := setupTest()
|
|
||||||
defer tearDown()
|
|
||||||
|
|
||||||
mux.HandleFunc("/dns-zones/zone/records", func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
if req.Method != http.MethodPatch {
|
|
||||||
http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
auth := req.Header.Get("X-Auth-Token")
|
|
||||||
if auth != fakeToken {
|
|
||||||
http.Error(rw, fmt.Sprintf("invalid token: %s", auth), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rw.WriteHeader(http.StatusNotFound)
|
|
||||||
err := json.NewEncoder(rw).Encode(APIError{"oops"})
|
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
record := Record{
|
|
||||||
Type: "TXT",
|
|
||||||
TTL: 30,
|
|
||||||
Name: "fqdn",
|
|
||||||
Data: fmt.Sprintf(`"%s"`, "value"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := client.AddRecord("zone", record)
|
|
||||||
require.EqualError(t, err, "request failed with status code 404: oops")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_SetRecord(t *testing.T) {
|
|
||||||
client, mux, tearDown := setupTest()
|
|
||||||
defer tearDown()
|
|
||||||
|
|
||||||
mux.HandleFunc("/dns-zones/zone/records", func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
if req.Method != http.MethodPatch {
|
|
||||||
http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
auth := req.Header.Get("X-Auth-Token")
|
|
||||||
if auth != fakeToken {
|
|
||||||
http.Error(rw, fmt.Sprintf("invalid token: %s", auth), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := ioutil.ReadAll(req.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := `{"dns_zone":"zone","changes":[{"set":{"name":"fqdn","type":"TXT","records":[{"data":"\"value\"","name":"fqdn","ttl":30,"type":"TXT"}]}}]}`
|
|
||||||
assert.Equal(t, expected+"\n", string(raw))
|
|
||||||
})
|
|
||||||
|
|
||||||
record := Record{
|
|
||||||
Type: "TXT",
|
|
||||||
TTL: 30,
|
|
||||||
Name: "fqdn",
|
|
||||||
Data: fmt.Sprintf(`"%s"`, "value"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := client.SetRecord("zone", record)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_DeleteRecord(t *testing.T) {
|
|
||||||
client, mux, tearDown := setupTest()
|
|
||||||
defer tearDown()
|
|
||||||
|
|
||||||
mux.HandleFunc("/dns-zones/zone/records", func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
if req.Method != http.MethodPatch {
|
|
||||||
http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
auth := req.Header.Get("X-Auth-Token")
|
|
||||||
if auth != fakeToken {
|
|
||||||
http.Error(rw, fmt.Sprintf("invalid token: %s", auth), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := ioutil.ReadAll(req.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := `{"dns_zone":"zone","changes":[{"delete":{"data":"\"value\"","name":"fqdn","type":"TXT"}}]}`
|
|
||||||
assert.Equal(t, expected+"\n", string(raw))
|
|
||||||
})
|
|
||||||
|
|
||||||
record := Record{
|
|
||||||
Type: "TXT",
|
|
||||||
TTL: 30,
|
|
||||||
Name: "fqdn",
|
|
||||||
Data: fmt.Sprintf(`"%s"`, "value"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := client.DeleteRecord("zone", record)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
|
@ -1,22 +1,19 @@
|
||||||
// Package scaleway implements a DNS provider for solving the DNS-01 challenge using Scaleway Domains API.
|
// Package scaleway implements a DNS provider for solving the DNS-01 challenge using Scaleway Domains API.
|
||||||
// Scaleway Domain API reference: https://developers.scaleway.com/en/products/domain/api/
|
|
||||||
// Token: https://www.scaleway.com/en/docs/generate-an-api-token/
|
// Token: https://www.scaleway.com/en/docs/generate-an-api-token/
|
||||||
package scaleway
|
package scaleway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||||
"github.com/go-acme/lego/v4/platform/config/env"
|
"github.com/go-acme/lego/v4/platform/config/env"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/scaleway/internal"
|
scwdomain "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1"
|
||||||
|
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultBaseURL = "https://api.scaleway.com"
|
|
||||||
defaultVersion = "v2alpha2"
|
|
||||||
minTTL = 60
|
minTTL = 60
|
||||||
defaultPollingInterval = 10 * time.Second
|
defaultPollingInterval = 10 * time.Second
|
||||||
defaultPropagationTimeout = 120 * time.Second
|
defaultPropagationTimeout = 120 * time.Second
|
||||||
|
@ -26,49 +23,41 @@ const (
|
||||||
const (
|
const (
|
||||||
envNamespace = "SCALEWAY_"
|
envNamespace = "SCALEWAY_"
|
||||||
|
|
||||||
EnvBaseURL = envNamespace + "BASE_URL"
|
|
||||||
EnvAPIToken = envNamespace + "API_TOKEN"
|
EnvAPIToken = envNamespace + "API_TOKEN"
|
||||||
EnvAPIVersion = envNamespace + "API_VERSION"
|
EnvProjectID = envNamespace + "PROJECT_ID"
|
||||||
|
|
||||||
EnvTTL = envNamespace + "TTL"
|
EnvTTL = envNamespace + "TTL"
|
||||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is used to configure the creation of the DNSProvider.
|
// Config is used to configure the creation of the DNSProvider.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
BaseURL string
|
ProjectID string
|
||||||
Version string
|
|
||||||
Token string
|
Token string
|
||||||
PropagationTimeout time.Duration
|
PropagationTimeout time.Duration
|
||||||
PollingInterval time.Duration
|
PollingInterval time.Duration
|
||||||
TTL int
|
TTL int
|
||||||
HTTPClient *http.Client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||||
func NewDefaultConfig() *Config {
|
func NewDefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
BaseURL: env.GetOrDefaultString(EnvBaseURL, defaultBaseURL),
|
|
||||||
Version: env.GetOrDefaultString(EnvAPIVersion, defaultVersion),
|
|
||||||
TTL: env.GetOrDefaultInt(EnvTTL, minTTL),
|
TTL: env.GetOrDefaultInt(EnvTTL, minTTL),
|
||||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout),
|
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout),
|
||||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval),
|
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval),
|
||||||
HTTPClient: &http.Client{
|
|
||||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNSProvider implements the challenge.Provider interface.
|
// DNSProvider implements the challenge.Provider interface.
|
||||||
type DNSProvider struct {
|
type DNSProvider struct {
|
||||||
config *Config
|
config *Config
|
||||||
client *internal.Client
|
client *scwdomain.API
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSProvider returns a DNSProvider instance configured for Scaleway Domains API.
|
// NewDNSProvider returns a DNSProvider instance configured for Scaleway Domains API.
|
||||||
// API token must be passed in the environment variable SCALEWAY_API_TOKEN.
|
// Credentials must be passed in the environment variables:
|
||||||
|
// SCALEWAY_API_TOKEN, SCALEWAY_PROJECT_ID.
|
||||||
func NewDNSProvider() (*DNSProvider, error) {
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
values, err := env.Get(EnvAPIToken)
|
values, err := env.Get(EnvAPIToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -77,6 +66,7 @@ func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
|
||||||
config := NewDefaultConfig()
|
config := NewDefaultConfig()
|
||||||
config.Token = values[EnvAPIToken]
|
config.Token = values[EnvAPIToken]
|
||||||
|
config.ProjectID = env.GetOrFile(EnvProjectID)
|
||||||
|
|
||||||
return NewDNSProviderConfig(config)
|
return NewDNSProviderConfig(config)
|
||||||
}
|
}
|
||||||
|
@ -95,12 +85,22 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||||
config.TTL = minTTL
|
config.TTL = minTTL
|
||||||
}
|
}
|
||||||
|
|
||||||
client := internal.NewClient(internal.ClientOpts{
|
configuration := []scw.ClientOption{
|
||||||
BaseURL: fmt.Sprintf("%s/domain/%s", config.BaseURL, config.Version),
|
scw.WithAuth("SCWXXXXXXXXXXXXXXXXX", config.Token),
|
||||||
Token: config.Token,
|
scw.WithUserAgent("Scaleway Lego's provider"),
|
||||||
}, config.HTTPClient)
|
}
|
||||||
|
|
||||||
return &DNSProvider{config: config, client: client}, nil
|
if config.ProjectID != "" {
|
||||||
|
configuration = append(configuration, scw.WithDefaultProjectID(config.ProjectID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Scaleway client
|
||||||
|
clientScw, err := scw.NewClient(configuration...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scaleway: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{config: config, client: scwdomain.NewAPI(clientScw)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout returns the Timeout and interval to use when checking for DNS propagation.
|
// Timeout returns the Timeout and interval to use when checking for DNS propagation.
|
||||||
|
@ -113,17 +113,27 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||||
|
|
||||||
txtRecord := internal.Record{
|
records := []*scwdomain.Record{{
|
||||||
Type: "TXT",
|
|
||||||
TTL: uint32(d.config.TTL),
|
|
||||||
Name: fqdn,
|
|
||||||
Data: fmt.Sprintf(`"%s"`, value),
|
Data: fmt.Sprintf(`"%s"`, value),
|
||||||
|
Name: fqdn,
|
||||||
|
TTL: uint32(d.config.TTL),
|
||||||
|
Type: scwdomain.RecordTypeTXT,
|
||||||
|
Comment: scw.StringPtr("used by lego"),
|
||||||
|
}}
|
||||||
|
|
||||||
|
req := &scwdomain.UpdateDNSZoneRecordsRequest{
|
||||||
|
DNSZone: domain,
|
||||||
|
Changes: []*scwdomain.RecordChange{{
|
||||||
|
Add: &scwdomain.RecordChangeAdd{Records: records},
|
||||||
|
}},
|
||||||
|
ReturnAllRecords: scw.BoolPtr(false),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := d.client.AddRecord(domain, txtRecord)
|
_, err := d.client.UpdateDNSZoneRecords(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("scaleway: %w", err)
|
return fmt.Errorf("scaleway: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,16 +141,24 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||||
|
|
||||||
txtRecord := internal.Record{
|
recordIdentifier := &scwdomain.RecordIdentifier{
|
||||||
Type: "TXT",
|
|
||||||
TTL: uint32(d.config.TTL),
|
|
||||||
Name: fqdn,
|
Name: fqdn,
|
||||||
Data: fmt.Sprintf(`"%s"`, value),
|
Type: scwdomain.RecordTypeTXT,
|
||||||
|
Data: scw.StringPtr(fmt.Sprintf(`"%s"`, value)),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := d.client.DeleteRecord(domain, txtRecord)
|
req := &scwdomain.UpdateDNSZoneRecordsRequest{
|
||||||
|
DNSZone: domain,
|
||||||
|
Changes: []*scwdomain.RecordChange{{
|
||||||
|
Delete: &scwdomain.RecordChangeDelete{IDFields: recordIdentifier},
|
||||||
|
}},
|
||||||
|
ReturnAllRecords: scw.BoolPtr(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.client.UpdateDNSZoneRecords(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("scaleway: %w", err)
|
return fmt.Errorf("scaleway: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,11 @@ lego --email myemail@example.com --dns scaleway --domains my.example.org run
|
||||||
[Configuration]
|
[Configuration]
|
||||||
[Configuration.Credentials]
|
[Configuration.Credentials]
|
||||||
SCALEWAY_API_TOKEN = "API token"
|
SCALEWAY_API_TOKEN = "API token"
|
||||||
|
SCALEWAY_PROJECT_ID = "Project to use (optional)"
|
||||||
[Configuration.Additional]
|
[Configuration.Additional]
|
||||||
SCALEWAY_BASE_URL = "API endpoint URL"
|
|
||||||
SCALEWAY_API_VERSION = "API version"
|
|
||||||
SCALEWAY_POLLING_INTERVAL = "Time between DNS propagation check"
|
SCALEWAY_POLLING_INTERVAL = "Time between DNS propagation check"
|
||||||
SCALEWAY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
SCALEWAY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
||||||
SCALEWAY_TTL = "The TTL of the TXT record used for the DNS challenge"
|
SCALEWAY_TTL = "The TTL of the TXT record used for the DNS challenge"
|
||||||
SCALEWAY_HTTP_TIMEOUT = "API request timeout"
|
|
||||||
|
|
||||||
[Links]
|
[Links]
|
||||||
API = "https://developers.scaleway.com/en/products/domain/api/"
|
API = "https://developers.scaleway.com/en/products/domain/dns/api/"
|
||||||
|
|
|
@ -10,11 +10,12 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const cleanUpDelay = 2 * time.Second
|
const envDomain = envNamespace + "DOMAIN"
|
||||||
|
|
||||||
|
var envTest = tester.NewEnvTest(EnvAPIToken, EnvProjectID).
|
||||||
|
WithDomain(envDomain)
|
||||||
|
|
||||||
func TestNewDNSProvider(t *testing.T) {
|
func TestNewDNSProvider(t *testing.T) {
|
||||||
envTest := tester.NewEnvTest(EnvAPIToken, EnvTTL)
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
envVars map[string]string
|
envVars map[string]string
|
||||||
|
@ -23,13 +24,15 @@ func TestNewDNSProvider(t *testing.T) {
|
||||||
{
|
{
|
||||||
desc: "success",
|
desc: "success",
|
||||||
envVars: map[string]string{
|
envVars: map[string]string{
|
||||||
EnvAPIToken: "123",
|
EnvAPIToken: "00000000-0000-0000-0000-000000000000",
|
||||||
|
EnvProjectID: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "missing api key",
|
desc: "missing api key",
|
||||||
envVars: map[string]string{
|
envVars: map[string]string{
|
||||||
EnvAPIToken: "",
|
EnvAPIToken: "",
|
||||||
|
EnvProjectID: "",
|
||||||
},
|
},
|
||||||
expected: fmt.Sprintf("scaleway: some credentials information are missing: %s", EnvAPIToken),
|
expected: fmt.Sprintf("scaleway: some credentials information are missing: %s", EnvAPIToken),
|
||||||
},
|
},
|
||||||
|
@ -65,7 +68,7 @@ func TestNewDNSProviderConfig(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "success",
|
desc: "success",
|
||||||
token: "123",
|
token: "00000000-0000-0000-0000-000000000000",
|
||||||
ttl: minTTL,
|
ttl: minTTL,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -97,8 +100,6 @@ func TestNewDNSProviderConfig(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLivePresent(t *testing.T) {
|
func TestLivePresent(t *testing.T) {
|
||||||
envTest := tester.NewEnvTest(EnvAPIToken, EnvTTL)
|
|
||||||
|
|
||||||
if !envTest.IsLiveTest() {
|
if !envTest.IsLiveTest() {
|
||||||
t.Skip("skipping live test")
|
t.Skip("skipping live test")
|
||||||
}
|
}
|
||||||
|
@ -112,8 +113,6 @@ func TestLivePresent(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLiveCleanUp(t *testing.T) {
|
func TestLiveCleanUp(t *testing.T) {
|
||||||
envTest := tester.NewEnvTest(EnvAPIToken, EnvTTL)
|
|
||||||
|
|
||||||
if !envTest.IsLiveTest() {
|
if !envTest.IsLiveTest() {
|
||||||
t.Skip("skipping live test")
|
t.Skip("skipping live test")
|
||||||
}
|
}
|
||||||
|
@ -122,7 +121,7 @@ func TestLiveCleanUp(t *testing.T) {
|
||||||
provider, err := NewDNSProvider()
|
provider, err := NewDNSProvider()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
time.Sleep(cleanUpDelay)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
|
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
Loading…
Add table
Reference in a new issue