From b2aab0377cb8774886983ffefac5695ebf8aaf3c Mon Sep 17 00:00:00 2001 From: James Nugent Date: Mon, 17 Jul 2017 14:50:53 -0500 Subject: [PATCH] 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. --- cli.go | 2 +- providers/dns/route53/route53.go | 24 +++++++++++++++---- .../dns/route53/route53_integration_test.go | 2 +- providers/dns/route53/route53_test.go | 18 ++++++++++++++ 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/cli.go b/cli.go index 58714b40..ae8354b5 100644 --- a/cli.go +++ b/cli.go @@ -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") diff --git a/providers/dns/route53/route53.go b/providers/dns/route53/route53.go index f3e53a8e..934f0a2d 100644 --- a/providers/dns/route53/route53.go +++ b/providers/dns/route53/route53.go @@ -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 } diff --git a/providers/dns/route53/route53_integration_test.go b/providers/dns/route53/route53_integration_test.go index 64678906..17ba4a08 100644 --- a/providers/dns/route53/route53_integration_test.go +++ b/providers/dns/route53/route53_integration_test.go @@ -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()) diff --git a/providers/dns/route53/route53_test.go b/providers/dns/route53/route53_test.go index ab8739a5..de4e28f3 100644 --- a/providers/dns/route53/route53_test.go +++ b/providers/dns/route53/route53_test.go @@ -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},