mirror of
https://github.com/go-acme/lego.git
synced 2024-12-25 11:02:56 +02:00
Add DNS provider for Scaleway (#1047)
This commit is contained in:
parent
59ea57daf6
commit
ee33cff002
@ -56,6 +56,6 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
|
||||
| [Manual](https://go-acme.github.io/lego/dns/manual/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) |
|
||||
| [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) |
|
||||
| [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) |
|
||||
| [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) |
|
||||
| [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) |
|
||||
| [Versio](https://go-acme.github.io/lego/dns/versio/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | |
|
||||
| [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) |
|
||||
| [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) |
|
||||
| [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Versio](https://go-acme.github.io/lego/dns/versio/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) |
|
||||
|
@ -68,6 +68,7 @@ func allDNSCodes() string {
|
||||
"rfc2136",
|
||||
"route53",
|
||||
"sakuracloud",
|
||||
"scaleway",
|
||||
"selectel",
|
||||
"stackpath",
|
||||
"transip",
|
||||
@ -1237,6 +1238,28 @@ func displayDNSHelp(name string) error {
|
||||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/sakuracloud`)
|
||||
|
||||
case "scaleway":
|
||||
// generated from: providers/dns/scaleway/scaleway.toml
|
||||
ew.writeln(`Configuration for Scaleway.`)
|
||||
ew.writeln(`Code: 'scaleway'`)
|
||||
ew.writeln(`Since: 'v3.4.0'`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Credentials:`)
|
||||
ew.writeln(` - "SCALEWAY_API_TOKEN": API token`)
|
||||
ew.writeln()
|
||||
|
||||
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_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()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/scaleway`)
|
||||
|
||||
case "selectel":
|
||||
// generated from: providers/dns/selectel/selectel.toml
|
||||
ew.writeln(`Configuration for Selectel.`)
|
||||
|
64
docs/content/dns/zz_gen_scaleway.md
Normal file
64
docs/content/dns/zz_gen_scaleway.md
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
title: "Scaleway"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: scaleway
|
||||
---
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/scaleway/scaleway.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
|
||||
Since: v3.4.0
|
||||
|
||||
Configuration for [Scaleway](https://developers.scaleway.com/).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
||||
- Code: `scaleway`
|
||||
|
||||
Here is an example bash command using the Scaleway provider:
|
||||
|
||||
```bash
|
||||
SCALEWAY_API_TOKEN=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \
|
||||
lego --dns scaleway.com --domains my.domain.com --email my@email.com run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `SCALEWAY_API_TOKEN` | API token |
|
||||
|
||||
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).
|
||||
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
| 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_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
|
||||
| `SCALEWAY_TTL` | The TTL of the TXT record used for the DNS challenge |
|
||||
|
||||
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
|
||||
|
||||
- [API documentation](https://developers.scaleway.com/en/products/domain/api/)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/scaleway/scaleway.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
@ -59,6 +59,7 @@ import (
|
||||
"github.com/go-acme/lego/v3/providers/dns/rfc2136"
|
||||
"github.com/go-acme/lego/v3/providers/dns/route53"
|
||||
"github.com/go-acme/lego/v3/providers/dns/sakuracloud"
|
||||
"github.com/go-acme/lego/v3/providers/dns/scaleway"
|
||||
"github.com/go-acme/lego/v3/providers/dns/selectel"
|
||||
"github.com/go-acme/lego/v3/providers/dns/stackpath"
|
||||
"github.com/go-acme/lego/v3/providers/dns/transip"
|
||||
@ -182,10 +183,12 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
|
||||
return rfc2136.NewDNSProvider()
|
||||
case "sakuracloud":
|
||||
return sakuracloud.NewDNSProvider()
|
||||
case "stackpath":
|
||||
return stackpath.NewDNSProvider()
|
||||
case "scaleway":
|
||||
return scaleway.NewDNSProvider()
|
||||
case "selectel":
|
||||
return selectel.NewDNSProvider()
|
||||
case "stackpath":
|
||||
return stackpath.NewDNSProvider()
|
||||
case "transip":
|
||||
return transip.NewDNSProvider()
|
||||
case "vegadns":
|
||||
|
228
providers/dns/scaleway/internal/client.go
Normal file
228
providers/dns/scaleway/internal/client.go
Normal file
@ -0,0 +1,228 @@
|
||||
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.Add("X-auth-token", c.token)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("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
|
||||
}
|
176
providers/dns/scaleway/internal/client_test.go
Normal file
176
providers/dns/scaleway/internal/client_test.go
Normal file
@ -0,0 +1,176 @@
|
||||
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)
|
||||
}
|
143
providers/dns/scaleway/scaleway.go
Normal file
143
providers/dns/scaleway/scaleway.go
Normal file
@ -0,0 +1,143 @@
|
||||
// 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/
|
||||
package scaleway
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v3/challenge/dns01"
|
||||
"github.com/go-acme/lego/v3/platform/config/env"
|
||||
"github.com/go-acme/lego/v3/providers/dns/scaleway/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBaseURL = "https://api.scaleway.com"
|
||||
defaultVersion = "v2alpha2"
|
||||
minTTL = 60
|
||||
defaultPollingInterval = 10 * time.Second
|
||||
defaultPropagationTimeout = 120 * time.Second
|
||||
)
|
||||
|
||||
const (
|
||||
envNamespace = "SCALEWAY_"
|
||||
baseURLEnvVar = envNamespace + "BASE_URL"
|
||||
apiTokenEnvVar = envNamespace + "API_TOKEN"
|
||||
apiVersionEnvVar = envNamespace + "API_VERSION"
|
||||
ttlEnvVar = envNamespace + "TTL"
|
||||
propagationTimeoutEnvVar = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
pollingIntervalEnvVar = envNamespace + "POLLING_INTERVAL"
|
||||
httpTimeoutEnvVar = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
// Config is used to configure the creation of the DNSProvider.
|
||||
type Config struct {
|
||||
BaseURL string
|
||||
Version string
|
||||
Token string
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
BaseURL: env.GetOrDefaultString(baseURLEnvVar, defaultBaseURL),
|
||||
Version: env.GetOrDefaultString(apiVersionEnvVar, defaultVersion),
|
||||
TTL: env.GetOrDefaultInt(ttlEnvVar, minTTL),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(propagationTimeoutEnvVar, defaultPropagationTimeout),
|
||||
PollingInterval: env.GetOrDefaultSecond(pollingIntervalEnvVar, defaultPollingInterval),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond(httpTimeoutEnvVar, 30*time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DNSProvider is an implementation of the challenge.Provider interface.
|
||||
type DNSProvider struct {
|
||||
config *Config
|
||||
client *internal.Client
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for Scaleway Domains API.
|
||||
// API token must be passed in the environment variable SCALEWAY_API_TOKEN.
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(apiTokenEnvVar)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scaleway: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.Token = values[apiTokenEnvVar]
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderConfig return a DNSProvider instance configured for scaleway.
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("scaleway: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
if config.Token == "" {
|
||||
return nil, errors.New("scaleway: credentials missing")
|
||||
}
|
||||
|
||||
if config.TTL < minTTL {
|
||||
config.TTL = minTTL
|
||||
}
|
||||
|
||||
client := internal.NewClient(internal.ClientOpts{
|
||||
BaseURL: fmt.Sprintf("%s/domain/%s", config.BaseURL, config.Version),
|
||||
Token: config.Token,
|
||||
}, config.HTTPClient)
|
||||
|
||||
return &DNSProvider{config: config, client: client}, nil
|
||||
}
|
||||
|
||||
// Timeout returns the Timeout and interval to use when checking for DNS propagation.
|
||||
// Adjusting here to cope with spikes in propagation times.
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
// Present creates a TXT record to fulfill DNS-01 challenge.
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
txtRecord := internal.Record{
|
||||
Type: "TXT",
|
||||
TTL: uint32(d.config.TTL),
|
||||
Name: fqdn,
|
||||
Data: fmt.Sprintf(`"%s"`, value),
|
||||
}
|
||||
|
||||
err := d.client.AddRecord(domain, txtRecord)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scaleway: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes a TXT record used for DNS-01 challenge.
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
txtRecord := internal.Record{
|
||||
Type: "TXT",
|
||||
TTL: uint32(d.config.TTL),
|
||||
Name: fqdn,
|
||||
Data: fmt.Sprintf(`"%s"`, value),
|
||||
}
|
||||
|
||||
err := d.client.DeleteRecord(domain, txtRecord)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scaleway: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
24
providers/dns/scaleway/scaleway.toml
Normal file
24
providers/dns/scaleway/scaleway.toml
Normal file
@ -0,0 +1,24 @@
|
||||
Name = "Scaleway"
|
||||
Description = ''''''
|
||||
URL = "https://developers.scaleway.com/"
|
||||
Code = "scaleway"
|
||||
Since = "v3.4.0"
|
||||
|
||||
Example = '''
|
||||
SCALEWAY_API_TOKEN=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \
|
||||
lego --dns scaleway.com --domains my.domain.com --email my@email.com run
|
||||
'''
|
||||
|
||||
[Configuration]
|
||||
[Configuration.Credentials]
|
||||
SCALEWAY_API_TOKEN = "API token"
|
||||
[Configuration.Additional]
|
||||
SCALEWAY_BASE_URL = "API endpoint URL"
|
||||
SCALEWAY_API_VERSION = "API version"
|
||||
SCALEWAY_POLLING_INTERVAL = "Time between DNS propagation check"
|
||||
SCALEWAY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
||||
SCALEWAY_TTL = "The TTL of the TXT record used for the DNS challenge"
|
||||
SCALEWAY_HTTP_TIMEOUT = "API request timeout"
|
||||
|
||||
[Links]
|
||||
API = "https://developers.scaleway.com/en/products/domain/api/"
|
131
providers/dns/scaleway/scaleway_test.go
Normal file
131
providers/dns/scaleway/scaleway_test.go
Normal file
@ -0,0 +1,131 @@
|
||||
package scaleway
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v3/platform/tester"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
cleanUpDelay = 2 * time.Second
|
||||
)
|
||||
|
||||
func TestNewDNSProvider(t *testing.T) {
|
||||
var envTest = tester.NewEnvTest(apiTokenEnvVar, ttlEnvVar)
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
envVars: map[string]string{
|
||||
apiTokenEnvVar: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "missing api key",
|
||||
envVars: map[string]string{
|
||||
apiTokenEnvVar: "",
|
||||
},
|
||||
expected: fmt.Sprintf("scaleway: some credentials information are missing: %s", apiTokenEnvVar),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
defer envTest.RestoreEnv()
|
||||
envTest.ClearEnv()
|
||||
|
||||
envTest.Apply(test.envVars)
|
||||
|
||||
p, err := NewDNSProvider()
|
||||
|
||||
if len(test.expected) == 0 {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
assert.NotNil(t, p.config)
|
||||
assert.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProviderConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
token string
|
||||
ttl int
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
token: "123",
|
||||
ttl: minTTL,
|
||||
},
|
||||
{
|
||||
desc: "missing api key",
|
||||
token: "",
|
||||
ttl: minTTL,
|
||||
expected: "scaleway: credentials missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
config := NewDefaultConfig()
|
||||
config.TTL = test.ttl
|
||||
config.Token = test.token
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
|
||||
if len(test.expected) == 0 {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
assert.NotNil(t, p.config)
|
||||
assert.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLivePresent(t *testing.T) {
|
||||
var envTest = tester.NewEnvTest(apiTokenEnvVar, ttlEnvVar)
|
||||
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Present(envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveCleanUp(t *testing.T) {
|
||||
var envTest = tester.NewEnvTest(apiTokenEnvVar, ttlEnvVar)
|
||||
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(cleanUpDelay)
|
||||
|
||||
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
Loading…
Reference in New Issue
Block a user