dns/route53: Allow specifying hosted zone ID (#345)

* dns/route53: Allow specifying hosted zone ID

This commit adds support for specifying hosted zone ID via the
environment variable AWS_HOSTED_ZONE_ID. If this is not specified, the
previous discovery process is used.

This is useful in environments where multiple hosted zones for the same
domain name are present in an account.

* dns/route53: Fix up getHostedZoneID method params

Now that getHostedZoneID is a method on the DNSProvider struct, there is
no reason for it to take the Route53 client as a parameter - we can
simply use the reference stored in the struct.
This commit is contained in:
James Nugent 2017-07-17 14:50:53 -05:00 committed by xenolf
parent dd74b99f8d
commit b2aab0377c
4 changed files with 39 additions and 7 deletions

2
cli.go
View file

@ -215,7 +215,7 @@ Here is an example bash command using the CloudFlare DNS provider:
fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_API_USER, NAMECHEAP_API_KEY")
fmt.Fprintln(w, "\trackspace:\tRACKSPACE_USER, RACKSPACE_API_KEY")
fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER")
fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION")
fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_HOSTED_ZONE_ID")
fmt.Fprintln(w, "\tdyn:\tDYN_CUSTOMER_NAME, DYN_USER_NAME, DYN_PASSWORD")
fmt.Fprintln(w, "\tvultr:\tVULTR_API_KEY")
fmt.Fprintln(w, "\tovh:\tOVH_ENDPOINT, OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY")

View file

@ -5,6 +5,7 @@ package route53
import (
"fmt"
"math/rand"
"os"
"strings"
"time"
@ -23,7 +24,8 @@ const (
// DNSProvider implements the acme.ChallengeProvider interface
type DNSProvider struct {
client *route53.Route53
client *route53.Route53
hostedZoneID string
}
// customRetryer implements the client.Retryer interface by composing the
@ -58,14 +60,22 @@ func (d customRetryer) RetryRules(r *request.Request) time.Duration {
// 2. Shared credentials file (defaults to ~/.aws/credentials)
// 3. Amazon EC2 IAM role
//
// If AWS_HOSTED_ZONE_ID is not set, Lego tries to determine the correct
// public hosted zone via the FQDN.
//
// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk
func NewDNSProvider() (*DNSProvider, error) {
hostedZoneID := os.Getenv("AWS_HOSTED_ZONE_ID")
r := customRetryer{}
r.NumMaxRetries = maxRetries
config := request.WithRetryer(aws.NewConfig(), r)
client := route53.New(session.New(config))
return &DNSProvider{client: client}, nil
return &DNSProvider{
client: client,
hostedZoneID: hostedZoneID,
}, nil
}
// Present creates a TXT record using the specified parameters
@ -83,7 +93,7 @@ func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
hostedZoneID, err := getHostedZoneID(fqdn, r.client)
hostedZoneID, err := r.getHostedZoneID(fqdn)
if err != nil {
return fmt.Errorf("Failed to determine Route 53 hosted zone ID: %v", err)
}
@ -124,7 +134,11 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
})
}
func getHostedZoneID(fqdn string, client *route53.Route53) (string, error) {
func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
if r.hostedZoneID != "" {
return r.hostedZoneID, nil
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return "", err
@ -134,7 +148,7 @@ func getHostedZoneID(fqdn string, client *route53.Route53) (string, error) {
reqParams := &route53.ListHostedZonesByNameInput{
DNSName: aws.String(acme.UnFqdn(authZone)),
}
resp, err := client.ListHostedZonesByName(reqParams)
resp, err := r.client.ListHostedZonesByName(reqParams)
if err != nil {
return "", err
}

View file

@ -30,7 +30,7 @@ func TestRoute53TTL(t *testing.T) {
// unexported.
fqdn := "_acme-challenge." + m["route53Domain"] + "."
svc := route53.New(session.New())
zoneID, err := getHostedZoneID(fqdn, svc)
zoneID, err := provider.getHostedZoneID(fqdn)
if err != nil {
provider.CleanUp(m["route53Domain"], "foo", "bar")
t.Fatalf("Fatal: %s", err.Error())

View file

@ -16,18 +16,21 @@ var (
route53Secret string
route53Key string
route53Region string
route53Zone string
)
func init() {
route53Key = os.Getenv("AWS_ACCESS_KEY_ID")
route53Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
route53Region = os.Getenv("AWS_REGION")
route53Zone = os.Getenv("AWS_HOSTED_ZONE_ID")
}
func restoreRoute53Env() {
os.Setenv("AWS_ACCESS_KEY_ID", route53Key)
os.Setenv("AWS_SECRET_ACCESS_KEY", route53Secret)
os.Setenv("AWS_REGION", route53Region)
os.Setenv("AWS_HOSTED_ZONE_ID", route53Zone)
}
func makeRoute53Provider(ts *httptest.Server) *DNSProvider {
@ -67,6 +70,21 @@ func TestRegionFromEnv(t *testing.T) {
restoreRoute53Env()
}
func TestHostedZoneIDFromEnv(t *testing.T) {
const testZoneID = "testzoneid"
defer restoreRoute53Env()
os.Setenv("AWS_HOSTED_ZONE_ID", testZoneID)
provider, err := NewDNSProvider()
assert.NoError(t, err, "Expected no error constructing DNSProvider")
fqdn, err := provider.getHostedZoneID("whatever")
assert.NoError(t, err, "Expected FQDN to be resolved to environment variable value")
assert.Equal(t, testZoneID, fqdn)
}
func TestRoute53Present(t *testing.T) {
mockResponses := MockResponseMap{
"/2013-04-01/hostedzonesbyname": MockResponse{StatusCode: 200, Body: ListHostedZonesByNameResponse},