1
0
mirror of https://github.com/go-acme/lego.git synced 2024-12-23 01:07:23 +02:00

chore: migrate to aws-sdk-go-v2 (lightsail, route53) (#1973)

This commit is contained in:
Ludovic Fernandez 2023-07-27 12:15:26 +02:00 committed by GitHub
parent ed14dda361
commit fc47c35e89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 317 additions and 213 deletions

View File

@ -95,7 +95,7 @@ Alternatively, you can also set the `Resource` to `*` (wildcard), which allow to
## More information
- [Go client](https://github.com/aws/aws-sdk-go/)
- [Go client](https://github.com/aws/aws-sdk-go-v2)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/lightsail/lightsail.toml -->

View File

@ -178,7 +178,7 @@ Replace `Z11111112222222333333` with your hosted zone ID and `example.com` with
## More information
- [API documentation](https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html)
- [Go client](https://github.com/aws/aws-sdk-go/aws)
- [Go client](https://github.com/aws/aws-sdk-go-v2)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/route53/route53.toml -->

16
go.mod
View File

@ -3,6 +3,7 @@ module github.com/go-acme/lego/v4
go 1.19
// github.com/exoscale/egoscale v1.19.0 => It is an error, please don't use it.
require (
cloud.google.com/go/compute/metadata v0.2.3
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
@ -17,7 +18,12 @@ require (
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755
github.com/aws/aws-sdk-go v1.39.0
github.com/aws/aws-sdk-go-v2 v1.19.0
github.com/aws/aws-sdk-go-v2/config v1.18.28
github.com/aws/aws-sdk-go-v2/credentials v1.13.27
github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3
github.com/cenkalti/backoff/v4 v4.2.1
github.com/civo/civogo v0.3.11
github.com/cloudflare/cloudflare-go v0.70.0
@ -89,6 +95,14 @@ require (
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect

31
go.sum
View File

@ -74,8 +74,34 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.39.0 h1:74BBwkEmiqBbi2CGflEh34l0YNtIibTjZsibGarkNjo=
github.com/aws/aws-sdk-go v1.39.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go-v2 v1.19.0 h1:klAT+y3pGFBU/qVf1uzwttpBbiuozJYWzNLHioyDJ+k=
github.com/aws/aws-sdk-go-v2 v1.19.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/config v1.18.28 h1:TINEaKyh1Td64tqFvn09iYpKiWjmHYrG1fa91q2gnqw=
github.com/aws/aws-sdk-go-v2/config v1.18.28/go.mod h1:nIL+4/8JdAuNHEjn/gPEXqtnS02Q3NXB/9Z7o5xE4+A=
github.com/aws/aws-sdk-go-v2/credentials v1.13.27 h1:dz0yr/yR1jweAnsCx+BmjerUILVPQ6FS5AwF/OyG1kA=
github.com/aws/aws-sdk-go-v2/credentials v1.13.27/go.mod h1:syOqAek45ZXZp29HlnRS/BNgMIW6uiRmeuQsz4Qh2UE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 h1:kP3Me6Fy3vdi+9uHd7YLr6ewPxRL+PU6y15urfTaamU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5/go.mod h1:Gj7tm95r+QsDoN2Fhuz/3npQvcZbkEf5mL70n3Xfluc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 h1:hMUCiE3Zi5AHrRNGf5j985u0WyqI6r2NULhUfo0N/No=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35/go.mod h1:ipR5PvpSPqIqL5Mi82BxLnfMkHVbmco8kUwO2xrCi0M=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 h1:yOpYx+FTBdpk/g+sBU6Cb1H0U/TLEcYYp66mYqsPpcc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29/go.mod h1:M/eUABlDbw2uVrdAn+UsI6M727qp2fxkp8K0ejcBDUY=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 h1:8r5m1BoAWkn0TDC34lUculryf7nUF25EgIMdjvGCkgo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36/go.mod h1:Rmw2M1hMVTwiUhjwMoIBFWFJMhvJbct06sSidxInkhY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 h1:IiDolu/eLmuB18DRZibj77n1hHQT7z12jnGO7Ze3pLc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29/go.mod h1:fDbkK4o7fpPXWn8YAPmTieAMuB9mk/VgvW64uaUqxd4=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2 h1:PwNeYoonBzmTdCztKiiutws3U24KrnDBuabzRfIlZY4=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2/go.mod h1:gQhLZrTEath4zik5ixIe6axvgY5jJrgSBDJ360Fxnco=
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4 h1:p4mTxJfCAyiTT4Wp6p/mOPa6j5MqCSRGot8qZwFs+Z0=
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4/go.mod h1:VBLWpaHvhQNeu7N9rMEf00SWeOONb/HvaDUxe/7b44k=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 h1:sWDv7cMITPcZ21QdreULwxOOAmE05JjEsT6fCDtDA9k=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13/go.mod h1:DfX0sWuT46KpcqbMhJ9QWtxAIP1VozkDWf8VAkByjYY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 h1:BFubHS/xN5bjl818QaroN6mQdjneYQ+AOx44KNXlyH4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13/go.mod h1:BzqsVVFduubEmzrVtUFQQIQdFqvUItF8XUq2EnS8Wog=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 h1:e5mnydVdCVWxP+5rPAGi2PYxC7u2OZgH1ypC114H04U=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3/go.mod h1:yVGZA1CPkmUhBdA039jXNJJG7/6t+G+EBWmFq23xqnY=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
@ -220,6 +246,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=

View File

@ -51,6 +51,11 @@ func (e *EnvTest) WithLiveTestRequirements(keys ...string) *EnvTest {
panic(fmt.Sprintf("Unauthorized action, the env var %s is not managed or it's not the key of the domain.", key))
}
if e.domainKey == key {
countValuedVars++
continue
}
if _, ok := e.values[key]; ok {
countValuedVars++
}

View File

@ -148,6 +148,22 @@ func TestEnvTest(t *testing.T) {
assert.Equal(t, "", envTest.GetDomain())
},
},
{
desc: "WithLiveTestRequirements with domain as requirement",
envVars: map[string]string{
envVar01: "A",
envVar02: "B",
},
envTestSetup: func() *tester.EnvTest {
return tester.NewEnvTest(envVar01, envVar02).WithDomain(envVarDomain).WithLiveTestRequirements(envVar02, envVarDomain)
},
expected: func(t *testing.T, envTest *tester.EnvTest) {
assert.True(t, envTest.IsLiveTest())
assert.Equal(t, "A", envTest.GetValue(envVar01))
assert.Equal(t, "B", envTest.GetValue(envVar02))
assert.Equal(t, "", envTest.GetDomain())
},
},
{
desc: "WithLiveTestRequirements non required var missing",
envVars: map[string]string{

View File

@ -2,17 +2,18 @@
package lightsail
import (
"context"
"errors"
"fmt"
"math/rand"
"strconv"
"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/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/retry"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/lightsail"
awstypes "github.com/aws/aws-sdk-go-v2/service/lightsail/types"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
)
@ -32,27 +33,6 @@ const (
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
)
// 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 (c 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
}
// Config is used to configure the creation of the DNSProvider.
type Config struct {
DNSZone string
@ -71,7 +51,7 @@ func NewDefaultConfig() *Config {
// DNSProvider implements the challenge.Provider interface.
type DNSProvider struct {
client *lightsail.Lightsail
client *lightsail.Client
config *Config
}
@ -102,35 +82,55 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return nil, errors.New("lightsail: the configuration of the DNS provider is nil")
}
retryer := customRetryer{}
retryer.NumMaxRetries = maxRetries
ctx := context.Background()
conf := aws.NewConfig().WithRegion(config.Region)
sess, err := session.NewSession(request.WithRetryer(conf, retryer))
cfg, err := awsconfig.LoadDefaultConfig(ctx,
awsconfig.WithRegion(config.Region),
awsconfig.WithRetryer(func() aws.Retryer {
return retry.NewStandard(func(options *retry.StandardOptions) {
options.MaxAttempts = maxRetries
// 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.
options.Backoff = retry.BackoffDelayerFunc(func(attempt int, err error) (time.Duration, error) {
retryCount := attempt
if retryCount > 7 {
retryCount = 7
}
delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200)
return time.Duration(delay) * time.Millisecond, nil
})
})
}),
)
if err != nil {
return nil, err
}
return &DNSProvider{
config: config,
client: lightsail.New(sess),
client: lightsail.NewFromConfig(cfg),
}, nil
}
// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
func (d *DNSProvider) Present(domain, _, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
params := &lightsail.CreateDomainEntryInput{
DomainName: aws.String(d.config.DNSZone),
DomainEntry: &lightsail.DomainEntry{
DomainEntry: &awstypes.DomainEntry{
Name: aws.String(info.EffectiveFQDN),
Target: aws.String(strconv.Quote(info.Value)),
Type: aws.String("TXT"),
},
}
_, err := d.client.CreateDomainEntry(params)
_, err := d.client.CreateDomainEntry(ctx, params)
if err != nil {
return fmt.Errorf("lightsail: %w", err)
}
@ -139,19 +139,20 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
params := &lightsail.DeleteDomainEntryInput{
DomainName: aws.String(d.config.DNSZone),
DomainEntry: &lightsail.DomainEntry{
DomainEntry: &awstypes.DomainEntry{
Name: aws.String(info.EffectiveFQDN),
Type: aws.String("TXT"),
Target: aws.String(strconv.Quote(info.Value)),
},
}
_, err := d.client.DeleteDomainEntry(params)
_, err := d.client.DeleteDomainEntry(ctx, params)
if err != nil {
return fmt.Errorf("lightsail: %w", err)
}

View File

@ -56,4 +56,4 @@ Alternatively, you can also set the `Resource` to `*` (wildcard), which allow to
LIGHTSAIL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
[Links]
GoClient = "https://github.com/aws/aws-sdk-go/"
GoClient = "https://github.com/aws/aws-sdk-go-v2"

View File

@ -1,11 +1,12 @@
package lightsail
import (
"context"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/lightsail"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/lightsail"
"github.com/stretchr/testify/require"
)
@ -24,13 +25,15 @@ func TestLiveTTL(t *testing.T) {
err = provider.Present(domain, "foo", "bar")
require.NoError(t, err)
// we need a separate Lightsail client here as the one in the DNS provider is
// unexported.
// we need a separate Lightsail client here as the one in the DNS provider is unexported.
fqdn := "_acme-challenge." + domain
sess, err := session.NewSession()
ctx := context.Background()
cfg, err := awsconfig.LoadDefaultConfig(ctx)
require.NoError(t, err)
svc := lightsail.New(sess)
svc := lightsail.NewFromConfig(cfg)
require.NoError(t, err)
defer func() {
@ -44,15 +47,24 @@ func TestLiveTTL(t *testing.T) {
DomainName: aws.String(domain),
}
resp, err := svc.GetDomain(params)
resp, err := svc.GetDomain(ctx, params)
require.NoError(t, err)
entries := resp.Domain.DomainEntries
for _, entry := range entries {
if aws.StringValue(entry.Type) == "TXT" && aws.StringValue(entry.Name) == fqdn {
if deref(entry.Type) == "TXT" && deref(entry.Name) == fqdn {
return
}
}
t.Fatalf("Could not find a TXT record for _acme-challenge.%s", domain)
}
func deref[T string | int | int32 | int64 | bool](v *T) T {
if v == nil {
var zero T
return zero
}
return *v
}

View File

@ -1,14 +1,16 @@
package lightsail
import (
"context"
"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/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/lightsail"
"github.com/go-acme/lego/v4/platform/tester"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -29,23 +31,26 @@ var envTest = tester.NewEnvTest(
WithDomain(EnvDNSZone).
WithLiveTestRequirements(envAwsAccessKeyID, envAwsSecretAccessKey, EnvDNSZone)
func makeProvider(serverURL string) (*DNSProvider, error) {
config := &aws.Config{
Credentials: credentials.NewStaticCredentials("abc", "123", " "),
Endpoint: aws.String(serverURL),
Region: aws.String("mock-region"),
MaxRetries: aws.Int(1),
type endpointResolverMock struct {
endpoint string
}
sess, err := session.NewSession(config)
if err != nil {
return nil, err
func (e endpointResolverMock) ResolveEndpoint(_, _ string, _ ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{URL: e.endpoint}, nil
}
conf := NewDefaultConfig()
func makeProvider(serverURL string) *DNSProvider {
config := aws.Config{
Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "),
Region: "mock-region",
EndpointResolverWithOptions: endpointResolverMock{endpoint: serverURL},
RetryMaxAttempts: 1,
}
client := lightsail.New(sess)
return &DNSProvider{client: client, config: conf}, nil
return &DNSProvider{
client: lightsail.NewFromConfig(config),
config: NewDefaultConfig(),
}
}
func TestCredentialsFromEnv(t *testing.T) {
@ -56,15 +61,19 @@ func TestCredentialsFromEnv(t *testing.T) {
_ = os.Setenv(envAwsSecretAccessKey, "123")
_ = os.Setenv(envAwsRegion, "us-east-1")
config := &aws.Config{
CredentialsChainVerboseErrors: aws.Bool(true),
}
sess, err := session.NewSession(config)
ctx := context.Background()
cfg, err := awsconfig.LoadDefaultConfig(ctx)
require.NoError(t, err)
_, err = sess.Config.Credentials.Get()
cs, err := cfg.Credentials.Retrieve(ctx)
require.NoError(t, err, "Expected credentials to be set from environment")
expected := aws.Credentials{
AccessKeyID: "123",
SecretAccessKey: "123",
Source: "EnvConfigCredentials",
}
assert.Equal(t, expected, cs)
}
func TestDNSProvider_Present(t *testing.T) {
@ -74,12 +83,11 @@ func TestDNSProvider_Present(t *testing.T) {
serverURL := newMockServer(t, mockResponses)
provider, err := makeProvider(serverURL)
require.NoError(t, err)
provider := makeProvider(serverURL)
domain := "example.com"
keyAuth := "123456d=="
err = provider.Present(domain, "", keyAuth)
err := provider.Present(domain, "", keyAuth)
require.NoError(t, err, "Expected Present to return no error")
}

View File

@ -2,19 +2,21 @@
package route53
import (
"context"
"errors"
"fmt"
"math/rand"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/retry"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/route53"
awstypes "github.com/aws/aws-sdk-go-v2/service/route53/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/go-acme/lego/v4/platform/wait"
@ -55,7 +57,7 @@ type Config struct {
PropagationTimeout time.Duration
PollingInterval time.Duration
Client *route53.Route53
Client *route53.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider.
@ -74,31 +76,10 @@ func NewDefaultConfig() *Config {
// DNSProvider implements the challenge.Provider interface.
type DNSProvider struct {
client *route53.Route53
client *route53.Client
config *Config
}
// 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 Route 53 service.
//
// AWS Credentials are automatically detected in the following locations and prioritized in the following order:
@ -124,13 +105,15 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{client: config.Client, config: config}, nil
}
sess, err := createSession(config)
ctx := context.Background()
cfg, err := createAWSConfig(ctx, config)
if err != nil {
return nil, err
}
return &DNSProvider{
client: route53.New(sess),
client: route53.NewFromConfig(cfg),
config: config,
}, nil
}
@ -142,14 +125,15 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
hostedZoneID, err := d.getHostedZoneID(info.EffectiveFQDN)
hostedZoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("route53: failed to determine hosted zone ID: %w", err)
}
records, err := d.getExistingRecordSets(hostedZoneID, info.EffectiveFQDN)
records, err := d.getExistingRecordSets(ctx, hostedZoneID, info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("route53: %w", err)
}
@ -158,39 +142,41 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
var found bool
for _, record := range records {
if aws.StringValue(record.Value) == realValue {
if deref(record.Value) == realValue {
found = true
}
}
if !found {
records = append(records, &route53.ResourceRecord{Value: aws.String(realValue)})
records = append(records, awstypes.ResourceRecord{Value: aws.String(realValue)})
}
recordSet := &route53.ResourceRecordSet{
recordSet := &awstypes.ResourceRecordSet{
Name: aws.String(info.EffectiveFQDN),
Type: aws.String("TXT"),
Type: "TXT",
TTL: aws.Int64(int64(d.config.TTL)),
ResourceRecords: records,
}
err = d.changeRecord(route53.ChangeActionUpsert, hostedZoneID, recordSet)
err = d.changeRecord(ctx, awstypes.ChangeActionUpsert, hostedZoneID, recordSet)
if err != nil {
return fmt.Errorf("route53: %w", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
hostedZoneID, err := d.getHostedZoneID(info.EffectiveFQDN)
hostedZoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("failed to determine Route 53 hosted zone ID: %w", err)
}
records, err := d.getExistingRecordSets(hostedZoneID, info.EffectiveFQDN)
records, err := d.getExistingRecordSets(ctx, hostedZoneID, info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("route53: %w", err)
}
@ -199,33 +185,33 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}
recordSet := &route53.ResourceRecordSet{
recordSet := &awstypes.ResourceRecordSet{
Name: aws.String(info.EffectiveFQDN),
Type: aws.String("TXT"),
Type: "TXT",
TTL: aws.Int64(int64(d.config.TTL)),
ResourceRecords: records,
}
err = d.changeRecord(route53.ChangeActionDelete, hostedZoneID, recordSet)
err = d.changeRecord(ctx, awstypes.ChangeActionDelete, hostedZoneID, recordSet)
if err != nil {
return fmt.Errorf("route53: %w", err)
}
return nil
}
func (d *DNSProvider) changeRecord(action, hostedZoneID string, recordSet *route53.ResourceRecordSet) error {
func (d *DNSProvider) changeRecord(ctx context.Context, action awstypes.ChangeAction, hostedZoneID string, recordSet *awstypes.ResourceRecordSet) error {
recordSetInput := &route53.ChangeResourceRecordSetsInput{
HostedZoneId: aws.String(hostedZoneID),
ChangeBatch: &route53.ChangeBatch{
ChangeBatch: &awstypes.ChangeBatch{
Comment: aws.String("Managed by Lego"),
Changes: []*route53.Change{{
Action: aws.String(action),
Changes: []awstypes.Change{{
Action: action,
ResourceRecordSet: recordSet,
}},
},
}
resp, err := d.client.ChangeResourceRecordSets(recordSetInput)
resp, err := d.client.ChangeResourceRecordSets(ctx, recordSetInput)
if err != nil {
return fmt.Errorf("failed to change record set: %w", err)
}
@ -235,26 +221,26 @@ func (d *DNSProvider) changeRecord(action, hostedZoneID string, recordSet *route
return wait.For("route53", d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) {
reqParams := &route53.GetChangeInput{Id: changeID}
resp, err := d.client.GetChange(reqParams)
resp, err := d.client.GetChange(ctx, reqParams)
if err != nil {
return false, fmt.Errorf("failed to query change status: %w", err)
}
if aws.StringValue(resp.ChangeInfo.Status) == route53.ChangeStatusInsync {
if resp.ChangeInfo.Status == awstypes.ChangeStatusInsync {
return true, nil
}
return false, fmt.Errorf("unable to retrieve change: ID=%s", aws.StringValue(changeID))
return false, fmt.Errorf("unable to retrieve change: ID=%s", deref(changeID))
})
}
func (d *DNSProvider) getExistingRecordSets(hostedZoneID, fqdn string) ([]*route53.ResourceRecord, error) {
func (d *DNSProvider) getExistingRecordSets(ctx context.Context, hostedZoneID, fqdn string) ([]awstypes.ResourceRecord, error) {
listInput := &route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String(hostedZoneID),
StartRecordName: aws.String(fqdn),
StartRecordType: aws.String("TXT"),
StartRecordType: "TXT",
}
recordSetsOutput, err := d.client.ListResourceRecordSets(listInput)
recordSetsOutput, err := d.client.ListResourceRecordSets(ctx, listInput)
if err != nil {
return nil, err
}
@ -263,10 +249,10 @@ func (d *DNSProvider) getExistingRecordSets(hostedZoneID, fqdn string) ([]*route
return nil, nil
}
var records []*route53.ResourceRecord
var records []awstypes.ResourceRecord
for _, recordSet := range recordSetsOutput.ResourceRecordSets {
if aws.StringValue(recordSet.Name) == fqdn {
if deref(recordSet.Name) == fqdn {
records = append(records, recordSet.ResourceRecords...)
}
}
@ -274,7 +260,7 @@ func (d *DNSProvider) getExistingRecordSets(hostedZoneID, fqdn string) ([]*route
return records, nil
}
func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string, error) {
if d.config.HostedZoneID != "" {
return d.config.HostedZoneID, nil
}
@ -288,7 +274,7 @@ func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
reqParams := &route53.ListHostedZonesByNameInput{
DNSName: aws.String(dns01.UnFqdn(authZone)),
}
resp, err := d.client.ListHostedZonesByName(reqParams)
resp, err := d.client.ListHostedZonesByName(ctx, reqParams)
if err != nil {
return "", err
}
@ -296,8 +282,8 @@ func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
var hostedZoneID string
for _, hostedZone := range resp.HostedZones {
// .Name has a trailing dot
if !aws.BoolValue(hostedZone.Config.PrivateZone) && aws.StringValue(hostedZone.Name) == authZone {
hostedZoneID = aws.StringValue(hostedZone.Id)
if !hostedZone.Config.PrivateZone && deref(hostedZone.Name) == authZone {
hostedZoneID = deref(hostedZone.Id)
break
}
}
@ -311,45 +297,60 @@ func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
return hostedZoneID, nil
}
func createSession(config *Config) (*session.Session, error) {
if err := createSessionCheckParams(config); err != nil {
return nil, err
func createAWSConfig(ctx context.Context, config *Config) (aws.Config, error) {
if err := createAWSConfigCheckParams(config); err != nil {
return aws.Config{}, err
}
retry := customRetryer{}
retry.NumMaxRetries = config.MaxRetries
optFns := []func(options *awsconfig.LoadOptions) error{
awsconfig.WithRetryer(func() aws.Retryer {
return retry.NewStandard(func(options *retry.StandardOptions) {
options.MaxAttempts = config.MaxRetries
// 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.
options.Backoff = retry.BackoffDelayerFunc(func(attempt int, err error) (time.Duration, error) {
retryCount := attempt
if retryCount > 7 {
retryCount = 7
}
delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200)
return time.Duration(delay) * time.Millisecond, nil
})
})
}),
}
awsConfig := aws.NewConfig()
if config.AccessKeyID != "" && config.SecretAccessKey != "" {
awsConfig = awsConfig.WithCredentials(credentials.NewStaticCredentials(config.AccessKeyID, config.SecretAccessKey, config.SessionToken))
optFns = append(optFns,
awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(config.AccessKeyID, config.SecretAccessKey, config.SessionToken)),
)
}
if config.Region != "" {
awsConfig = awsConfig.WithRegion(config.Region)
optFns = append(optFns, awsconfig.WithRegion(config.Region))
}
sessionCfg := request.WithRetryer(awsConfig, retry)
sess, err := session.NewSessionWithOptions(session.Options{Config: *sessionCfg})
cfg, err := awsconfig.LoadDefaultConfig(ctx, optFns...)
if err != nil {
return nil, err
return aws.Config{}, err
}
if config.AssumeRoleArn == "" {
return sess, nil
}
return session.NewSession(&aws.Config{
Region: sess.Config.Region,
Credentials: stscreds.NewCredentials(sess, config.AssumeRoleArn, func(arp *stscreds.AssumeRoleProvider) {
if config.AssumeRoleArn != "" {
cfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), config.AssumeRoleArn, func(options *stscreds.AssumeRoleOptions) {
if config.ExternalID != "" {
arp.ExternalID = &config.ExternalID
options.ExternalID = &config.ExternalID
}
}),
})
}
func createSessionCheckParams(config *Config) error {
return cfg, nil
}
func createAWSConfigCheckParams(config *Config) error {
if config == nil {
return errors.New("config is nil")
}
@ -364,3 +365,12 @@ func createSessionCheckParams(config *Config) error {
return nil
}
func deref[T string | int | int32 | int64 | bool](v *T) T {
if v == nil {
var zero T
return zero
}
return *v
}

View File

@ -140,4 +140,4 @@ Replace `Z11111112222222333333` with your hosted zone ID and `example.com` with
[Links]
API = "https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html"
GoClient = "https://github.com/aws/aws-sdk-go/aws"
GoClient = "https://github.com/aws/aws-sdk-go-v2"

View File

@ -1,11 +1,12 @@
package route53
import (
"context"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/route53"
"github.com/stretchr/testify/require"
)
@ -26,9 +27,13 @@ func TestLiveTTL(t *testing.T) {
// we need a separate R53 client here as the one in the DNS provider is unexported.
fqdn := "_acme-challenge." + domain + "."
sess, err := session.NewSession()
ctx := context.Background()
cfg, err := awsconfig.LoadDefaultConfig(ctx)
require.NoError(t, err)
svc := route53.New(sess)
svc := route53.NewFromConfig(cfg)
defer func() {
errC := provider.CleanUp(domain, "foo", "bar")
@ -37,17 +42,17 @@ func TestLiveTTL(t *testing.T) {
}
}()
zoneID, err := provider.getHostedZoneID(fqdn)
zoneID, err := provider.getHostedZoneID(context.Background(), fqdn)
require.NoError(t, err)
params := &route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String(zoneID),
}
resp, err := svc.ListResourceRecordSets(params)
resp, err := svc.ListResourceRecordSets(ctx, params)
require.NoError(t, err)
for _, v := range resp.ResourceRecordSets {
if aws.StringValue(v.Name) == fqdn && aws.StringValue(v.Type) == "TXT" && aws.Int64Value(v.TTL) == 10 {
if deref(v.Name) == fqdn && v.Type == "TXT" && deref(v.TTL) == 10 {
return
}
}

View File

@ -1,14 +1,15 @@
package route53
import (
"context"
"os"
"testing"
"time"
"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/route53"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/route53"
"github.com/go-acme/lego/v4/platform/tester"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -28,21 +29,26 @@ var envTest = tester.NewEnvTest(
WithDomain(envDomain).
WithLiveTestRequirements(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion, envDomain)
type endpointResolverMock struct {
endpoint string
}
func (e endpointResolverMock) ResolveEndpoint(_, _ string, _ ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{URL: e.endpoint}, nil
}
func makeTestProvider(t *testing.T, serverURL string) *DNSProvider {
t.Helper()
config := &aws.Config{
Credentials: credentials.NewStaticCredentials("abc", "123", " "),
Endpoint: aws.String(serverURL),
Region: aws.String("mock-region"),
MaxRetries: aws.Int(1),
cfg := aws.Config{
Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "),
Region: "mock-region",
EndpointResolverWithOptions: endpointResolverMock{endpoint: serverURL},
RetryMaxAttempts: 1,
}
sess, err := session.NewSession(config)
require.NoError(t, err)
return &DNSProvider{
client: route53.New(sess),
client: route53.NewFromConfig(cfg),
config: NewDefaultConfig(),
}
}
@ -55,22 +61,21 @@ func Test_loadCredentials_FromEnv(t *testing.T) {
_ = os.Setenv(EnvSecretAccessKey, "456")
_ = os.Setenv(EnvRegion, "us-east-1")
config := &aws.Config{
CredentialsChainVerboseErrors: aws.Bool(true),
}
ctx := context.Background()
sess, err := session.NewSession(config)
cfg, err := awsconfig.LoadDefaultConfig(ctx)
require.NoError(t, err)
value, err := sess.Config.Credentials.Get()
value, err := cfg.Credentials.Retrieve(ctx)
require.NoError(t, err, "Expected credentials to be set from environment")
expected := credentials.Value{
expected := aws.Credentials{
AccessKeyID: "123",
SecretAccessKey: "456",
SessionToken: "",
ProviderName: "EnvConfigCredentials",
Source: "EnvConfigCredentials",
}
assert.Equal(t, expected, value)
}
@ -78,13 +83,12 @@ func Test_loadRegion_FromEnv(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
os.Setenv(EnvRegion, route53.CloudWatchRegionUsEast1)
_ = os.Setenv(EnvRegion, "foo")
sess, err := session.NewSession(aws.NewConfig())
cfg, err := awsconfig.LoadDefaultConfig(context.Background())
require.NoError(t, err)
region := aws.StringValue(sess.Config.Region)
assert.Equal(t, route53.CloudWatchRegionUsEast1, region, "Region")
assert.Equal(t, "foo", cfg.Region, "Region")
}
func Test_getHostedZoneID_FromEnv(t *testing.T) {
@ -93,12 +97,12 @@ func Test_getHostedZoneID_FromEnv(t *testing.T) {
expectedZoneID := "zoneID"
os.Setenv(EnvHostedZoneID, expectedZoneID)
_ = os.Setenv(EnvHostedZoneID, expectedZoneID)
provider, err := NewDNSProvider()
require.NoError(t, err)
hostedZoneID, err := provider.getHostedZoneID("whatever")
hostedZoneID, err := provider.getHostedZoneID(context.Background(), "whatever")
require.NoError(t, err, "HostedZoneID")
assert.Equal(t, expectedZoneID, hostedZoneID)
@ -144,7 +148,7 @@ func TestNewDefaultConfig(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
envTest.ClearEnv()
for key, value := range test.envVars {
os.Setenv(key, value)
_ = os.Setenv(key, value)
}
config := NewDefaultConfig()
@ -157,7 +161,7 @@ func TestNewDefaultConfig(t *testing.T) {
func TestDNSProvider_Present(t *testing.T) {
mockResponses := MockResponseMap{
"/2013-04-01/hostedzonesbyname": {StatusCode: 200, Body: ListHostedZonesByNameResponse},
"/2013-04-01/hostedzone/ABCDEFG/rrset/": {StatusCode: 200, Body: ChangeResourceRecordSetsResponse},
"/2013-04-01/hostedzone/ABCDEFG/rrset": {StatusCode: 200, Body: ChangeResourceRecordSetsResponse},
"/2013-04-01/change/123456": {StatusCode: 200, Body: GetChangeResponse},
"/2013-04-01/hostedzone/ABCDEFG/rrset?name=_acme-challenge.example.com.&type=TXT": {
StatusCode: 200,
@ -178,12 +182,12 @@ func TestDNSProvider_Present(t *testing.T) {
require.NoError(t, err, "Expected Present to return no error")
}
func TestCreateSession(t *testing.T) {
func Test_createAWSConfig(t *testing.T) {
testCases := []struct {
desc string
env map[string]string
config *Config
wantCreds credentials.Value
wantCreds aws.Credentials
wantDefaultChain bool
wantRegion string
wantErr string
@ -218,11 +222,11 @@ func TestCreateSession(t *testing.T) {
AccessKeyID: "one",
SecretAccessKey: "two",
},
wantCreds: credentials.Value{
wantCreds: aws.Credentials{
AccessKeyID: "one",
SecretAccessKey: "two",
SessionToken: "",
ProviderName: credentials.StaticProviderName,
Source: credentials.StaticCredentialsName,
},
},
{
@ -232,11 +236,11 @@ func TestCreateSession(t *testing.T) {
SecretAccessKey: "two",
SessionToken: "three",
},
wantCreds: credentials.Value{
wantCreds: aws.Credentials{
AccessKeyID: "one",
SecretAccessKey: "two",
SessionToken: "three",
ProviderName: credentials.StaticProviderName,
Source: credentials.StaticCredentialsName,
},
},
{
@ -268,24 +272,26 @@ func TestCreateSession(t *testing.T) {
envTest.Apply(test.env)
sess, err := createSession(test.config)
ctx := context.Background()
cfg, err := createAWSConfig(ctx, test.config)
requireErr(t, err, test.wantErr)
if err != nil {
return
}
gotCreds, err := sess.Config.Credentials.Get()
gotCreds, err := cfg.Credentials.Retrieve(ctx)
if test.wantDefaultChain {
assert.NotEqual(t, credentials.StaticProviderName, gotCreds.ProviderName)
assert.NotEqual(t, credentials.StaticCredentialsName, gotCreds.Source)
} else {
require.NoError(t, err)
assert.Equal(t, test.wantCreds, gotCreds)
}
if test.wantRegion != "" {
assert.Equal(t, test.wantRegion, aws.StringValue(sess.Config.Region))
assert.Equal(t, test.wantRegion, cfg.Region)
}
})
}