mirror of
https://github.com/go-acme/lego.git
synced 2025-01-18 20:39:46 +02:00
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
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, "\tgandiv5:\tGANDIV5_API_KEY")
|
||||||
fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT, GCE_SERVICE_ACCOUNT_FILE")
|
fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT, GCE_SERVICE_ACCOUNT_FILE")
|
||||||
fmt.Fprintln(w, "\tlinode:\tLINODE_API_KEY")
|
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, "\tmanual:\tnone")
|
||||||
fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_API_USER, NAMECHEAP_API_KEY")
|
fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_API_USER, NAMECHEAP_API_KEY")
|
||||||
fmt.Fprintln(w, "\trackspace:\tRACKSPACE_USER, RACKSPACE_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/exoscale"
|
||||||
"github.com/xenolf/lego/providers/dns/gandi"
|
"github.com/xenolf/lego/providers/dns/gandi"
|
||||||
"github.com/xenolf/lego/providers/dns/gandiv5"
|
"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/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/linode"
|
||||||
"github.com/xenolf/lego/providers/dns/namecheap"
|
"github.com/xenolf/lego/providers/dns/namecheap"
|
||||||
"github.com/xenolf/lego/providers/dns/ns1"
|
"github.com/xenolf/lego/providers/dns/ns1"
|
||||||
@ -63,6 +64,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
|||||||
provider, err = googlecloud.NewDNSProvider()
|
provider, err = googlecloud.NewDNSProvider()
|
||||||
case "godaddy":
|
case "godaddy":
|
||||||
provider, err = godaddy.NewDNSProvider()
|
provider, err = godaddy.NewDNSProvider()
|
||||||
|
case "lightsail":
|
||||||
|
provider, err = lightsail.NewDNSProvider()
|
||||||
case "linode":
|
case "linode":
|
||||||
provider, err = linode.NewDNSProvider()
|
provider, err = linode.NewDNSProvider()
|
||||||
case "manual":
|
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 New Issue
Block a user