forked from TrueCloudLab/lego
Add DNS provider: Lightsail (#460)
* add lightsail dns provider * fix lint errors * update exoscale.go * add the docs for lightsail provider
This commit is contained in:
parent
4e330710a7
commit
bacb545c7a
6 changed files with 294 additions and 1 deletions
1
cli.go
1
cli.go
|
@ -213,6 +213,7 @@ Here is an example bash command using the CloudFlare DNS provider:
|
|||
fmt.Fprintln(w, "\tgandiv5:\tGANDIV5_API_KEY")
|
||||
fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT, GCE_SERVICE_ACCOUNT_FILE")
|
||||
fmt.Fprintln(w, "\tlinode:\tLINODE_API_KEY")
|
||||
fmt.Fprintln(w, "\tlightsail:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, DNS_ZONE")
|
||||
fmt.Fprintln(w, "\tmanual:\tnone")
|
||||
fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_API_USER, NAMECHEAP_API_KEY")
|
||||
fmt.Fprintln(w, "\trackspace:\tRACKSPACE_USER, RACKSPACE_API_KEY")
|
||||
|
|
|
@ -17,8 +17,9 @@ import (
|
|||
"github.com/xenolf/lego/providers/dns/exoscale"
|
||||
"github.com/xenolf/lego/providers/dns/gandi"
|
||||
"github.com/xenolf/lego/providers/dns/gandiv5"
|
||||
"github.com/xenolf/lego/providers/dns/googlecloud"
|
||||
"github.com/xenolf/lego/providers/dns/godaddy"
|
||||
"github.com/xenolf/lego/providers/dns/googlecloud"
|
||||
"github.com/xenolf/lego/providers/dns/lightsail"
|
||||
"github.com/xenolf/lego/providers/dns/linode"
|
||||
"github.com/xenolf/lego/providers/dns/namecheap"
|
||||
"github.com/xenolf/lego/providers/dns/ns1"
|
||||
|
@ -63,6 +64,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
|||
provider, err = googlecloud.NewDNSProvider()
|
||||
case "godaddy":
|
||||
provider, err = godaddy.NewDNSProvider()
|
||||
case "lightsail":
|
||||
provider, err = lightsail.NewDNSProvider()
|
||||
case "linode":
|
||||
provider, err = linode.NewDNSProvider()
|
||||
case "manual":
|
||||
|
|
107
providers/dns/lightsail/lightsail.go
Normal file
107
providers/dns/lightsail/lightsail.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
// Package lightsail implements a DNS provider for solving the DNS-01 challenge
|
||||
// using AWS Lightsail DNS.
|
||||
package lightsail
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/client"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/lightsail"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
const (
|
||||
maxRetries = 5
|
||||
)
|
||||
|
||||
// DNSProvider implements the acme.ChallengeProvider interface
|
||||
type DNSProvider struct {
|
||||
client *lightsail.Lightsail
|
||||
}
|
||||
|
||||
// customRetryer implements the client.Retryer interface by composing the
|
||||
// DefaultRetryer. It controls the logic for retrying recoverable request
|
||||
// errors (e.g. when rate limits are exceeded).
|
||||
type customRetryer struct {
|
||||
client.DefaultRetryer
|
||||
}
|
||||
|
||||
// RetryRules overwrites the DefaultRetryer's method.
|
||||
// It uses a basic exponential backoff algorithm that returns an initial
|
||||
// delay of ~400ms with an upper limit of ~30 seconds which should prevent
|
||||
// causing a high number of consecutive throttling errors.
|
||||
// For reference: Route 53 enforces an account-wide(!) 5req/s query limit.
|
||||
func (d customRetryer) RetryRules(r *request.Request) time.Duration {
|
||||
retryCount := r.RetryCount
|
||||
if retryCount > 7 {
|
||||
retryCount = 7
|
||||
}
|
||||
|
||||
delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200)
|
||||
return time.Duration(delay) * time.Millisecond
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for the AWS
|
||||
// Lightsail service.
|
||||
//
|
||||
// AWS Credentials are automatically detected in the following locations
|
||||
// and prioritized in the following order:
|
||||
// 1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
|
||||
// [AWS_SESSION_TOKEN], [DNS_ZONE]
|
||||
// 2. Shared credentials file (defaults to ~/.aws/credentials)
|
||||
// 3. Amazon EC2 IAM role
|
||||
//
|
||||
// public hosted zone via the FQDN.
|
||||
//
|
||||
// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
r := customRetryer{}
|
||||
r.NumMaxRetries = maxRetries
|
||||
config := request.WithRetryer(aws.NewConfig(), r)
|
||||
client := lightsail.New(session.New(config))
|
||||
|
||||
return &DNSProvider{
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters
|
||||
func (r *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||
value = `"` + value + `"`
|
||||
err := r.newTxtRecord(domain, fqdn, value)
|
||||
return err
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters
|
||||
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||
value = `"` + value + `"`
|
||||
params := &lightsail.DeleteDomainEntryInput{
|
||||
DomainName: aws.String(domain),
|
||||
DomainEntry: &lightsail.DomainEntry{
|
||||
Name: aws.String(fqdn),
|
||||
Type: aws.String("TXT"),
|
||||
Target: aws.String(value),
|
||||
},
|
||||
}
|
||||
_, err := r.client.DeleteDomainEntry(params)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *DNSProvider) newTxtRecord(domain string, fqdn string, value string) error {
|
||||
params := &lightsail.CreateDomainEntryInput{
|
||||
DomainName: aws.String(domain),
|
||||
DomainEntry: &lightsail.DomainEntry{
|
||||
Name: aws.String(fqdn),
|
||||
Target: aws.String(value),
|
||||
Type: aws.String("TXT"),
|
||||
},
|
||||
}
|
||||
_, err := r.client.CreateDomainEntry(params)
|
||||
return err
|
||||
}
|
68
providers/dns/lightsail/lightsail_integration_test.go
Normal file
68
providers/dns/lightsail/lightsail_integration_test.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package lightsail
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/lightsail"
|
||||
)
|
||||
|
||||
func TestLightsailTTL(t *testing.T) {
|
||||
|
||||
m, err := testGetAndPreCheck()
|
||||
if err != nil {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
if err != nil {
|
||||
t.Fatalf("Fatal: %s", err.Error())
|
||||
}
|
||||
|
||||
err = provider.Present(m["lightsailDomain"], "foo", "bar")
|
||||
if err != nil {
|
||||
t.Fatalf("Fatal: %s", err.Error())
|
||||
}
|
||||
// we need a separate Lightshail client here as the one in the DNS provider is
|
||||
// unexported.
|
||||
fqdn := "_acme-challenge." + m["lightsailDomain"]
|
||||
svc := lightsail.New(session.New())
|
||||
if err != nil {
|
||||
provider.CleanUp(m["lightsailDomain"], "foo", "bar")
|
||||
t.Fatalf("Fatal: %s", err.Error())
|
||||
}
|
||||
params := &lightsail.GetDomainInput{
|
||||
DomainName: aws.String(m["lightsailDomain"]),
|
||||
}
|
||||
resp, err := svc.GetDomain(params)
|
||||
if err != nil {
|
||||
provider.CleanUp(m["lightsailDomain"], "foo", "bar")
|
||||
t.Fatalf("Fatal: %s", err.Error())
|
||||
}
|
||||
entries := resp.Domain.DomainEntries
|
||||
for _, entry := range entries {
|
||||
if *entry.Type == "TXT" && *entry.Name == fqdn {
|
||||
provider.CleanUp(m["lightsailDomain"], "foo", "bar")
|
||||
return
|
||||
}
|
||||
}
|
||||
provider.CleanUp(m["lightsailDomain"], "foo", "bar")
|
||||
t.Fatalf("Could not find a TXT record for _acme-challenge.%s", m["lightsailDomain"])
|
||||
}
|
||||
|
||||
func testGetAndPreCheck() (map[string]string, error) {
|
||||
m := map[string]string{
|
||||
"lightsailKey": os.Getenv("AWS_ACCESS_KEY_ID"),
|
||||
"lightsailSecret": os.Getenv("AWS_SECRET_ACCESS_KEY"),
|
||||
"lightsailDomain": os.Getenv("DNS_ZONE"),
|
||||
}
|
||||
for _, v := range m {
|
||||
if v == "" {
|
||||
return nil, fmt.Errorf("AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and R53_DOMAIN are needed to run this test")
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
76
providers/dns/lightsail/lightsail_test.go
Normal file
76
providers/dns/lightsail/lightsail_test.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package lightsail
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/lightsail"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
lightsailSecret string
|
||||
lightsailKey string
|
||||
lightsailZone string
|
||||
)
|
||||
|
||||
func init() {
|
||||
lightsailKey = os.Getenv("AWS_ACCESS_KEY_ID")
|
||||
lightsailSecret = os.Getenv("AWS_SECRET_ACCESS_KEY")
|
||||
}
|
||||
|
||||
func restoreLightsailEnv() {
|
||||
os.Setenv("AWS_ACCESS_KEY_ID", lightsailKey)
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", lightsailSecret)
|
||||
os.Setenv("AWS_REGION", "us-east-1")
|
||||
os.Setenv("AWS_HOSTED_ZONE_ID", lightsailZone)
|
||||
}
|
||||
|
||||
func makeLightsailProvider(ts *httptest.Server) *DNSProvider {
|
||||
config := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials("abc", "123", " "),
|
||||
Endpoint: aws.String(ts.URL),
|
||||
Region: aws.String("mock-region"),
|
||||
MaxRetries: aws.Int(1),
|
||||
}
|
||||
|
||||
client := lightsail.New(session.New(config))
|
||||
return &DNSProvider{client: client}
|
||||
}
|
||||
|
||||
func TestCredentialsFromEnv(t *testing.T) {
|
||||
os.Setenv("AWS_ACCESS_KEY_ID", "123")
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", "123")
|
||||
os.Setenv("AWS_REGION", "us-east-1")
|
||||
|
||||
config := &aws.Config{
|
||||
CredentialsChainVerboseErrors: aws.Bool(true),
|
||||
}
|
||||
|
||||
sess := session.New(config)
|
||||
_, err := sess.Config.Credentials.Get()
|
||||
assert.NoError(t, err, "Expected credentials to be set from environment")
|
||||
|
||||
restoreLightsailEnv()
|
||||
}
|
||||
|
||||
func TestLightsailPresent(t *testing.T) {
|
||||
mockResponses := MockResponseMap{
|
||||
"/": MockResponse{StatusCode: 200, Body: ""},
|
||||
}
|
||||
|
||||
ts := newMockServer(t, mockResponses)
|
||||
defer ts.Close()
|
||||
|
||||
provider := makeLightsailProvider(ts)
|
||||
|
||||
domain := "example.com"
|
||||
keyAuth := "123456d=="
|
||||
|
||||
err := provider.Present(domain, "", keyAuth)
|
||||
assert.NoError(t, err, "Expected Present to return no error")
|
||||
}
|
38
providers/dns/lightsail/testutil_test.go
Normal file
38
providers/dns/lightsail/testutil_test.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package lightsail
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// MockResponse represents a predefined response used by a mock server
|
||||
type MockResponse struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
}
|
||||
|
||||
// MockResponseMap maps request paths to responses
|
||||
type MockResponseMap map[string]MockResponse
|
||||
|
||||
func newMockServer(t *testing.T, responses MockResponseMap) *httptest.Server {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
resp, ok := responses[path]
|
||||
if !ok {
|
||||
msg := fmt.Sprintf("Requested path not found in response map: %s", path)
|
||||
require.FailNow(t, msg)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
w.Write([]byte(resp.Body))
|
||||
}))
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return ts
|
||||
}
|
Loading…
Reference in a new issue