diff --git a/pkg/apis/certmanager/types.go b/pkg/apis/certmanager/types.go index 94232a43b..4366631e6 100644 --- a/pkg/apis/certmanager/types.go +++ b/pkg/apis/certmanager/types.go @@ -80,6 +80,7 @@ type ACMEIssuerDNS01Provider struct { CloudDNS *ACMEIssuerDNS01ProviderCloudDNS Cloudflare *ACMEIssuerDNS01ProviderCloudflare + Route53 *ACMEIssuerDNS01ProviderRoute53 } // ACMEIssuerDNS01ProviderCloudDNS is a structure containing the DNS @@ -96,6 +97,16 @@ type ACMEIssuerDNS01ProviderCloudflare struct { APIKey SecretKeySelector } +// ACMEIssuerDNS01ProviderRoute53 is a structure containing the Route 53 +// configuration for AWS +type ACMEIssuerDNS01ProviderRoute53 struct { + AccessKeyID string + SecretAccessKey SecretKeySelector + // these following parameters are optional + HostedZoneID string + Region string +} + // +genclient=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/certmanager/v1alpha1/types.go b/pkg/apis/certmanager/v1alpha1/types.go index 616f3d305..c9a967f12 100644 --- a/pkg/apis/certmanager/v1alpha1/types.go +++ b/pkg/apis/certmanager/v1alpha1/types.go @@ -104,6 +104,7 @@ type ACMEIssuerDNS01Provider struct { CloudDNS *ACMEIssuerDNS01ProviderCloudDNS `json:"clouddns,omitempty"` Cloudflare *ACMEIssuerDNS01ProviderCloudflare `json:"cloudflare,omitempty"` + Route53 *ACMEIssuerDNS01ProviderRoute53 `json:"route53,omitempty"` } // ACMEIssuerDNS01ProviderCloudDNS is a structure containing the DNS @@ -120,6 +121,15 @@ type ACMEIssuerDNS01ProviderCloudflare struct { APIKey SecretKeySelector `json:"apiKey"` } +// ACMEIssuerDNS01ProviderRoute53 is a structure containing the Route 53 +// configuration for AWS +type ACMEIssuerDNS01ProviderRoute53 struct { + AccessKeyID string `json:"accessKeyID"` + SecretAccessKey SecretKeySelector `json:"secretAccessKey"` + HostedZoneID string `json:"hostedZoneID"` + Region string `json:"region"` +} + // +genclient=true // +k8s:openapi-gen=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/issuer/acme/dns/dns.go b/pkg/issuer/acme/dns/dns.go index a5c52f852..58526562f 100644 --- a/pkg/issuer/acme/dns/dns.go +++ b/pkg/issuer/acme/dns/dns.go @@ -12,6 +12,7 @@ import ( "github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1" "github.com/jetstack-experimental/cert-manager/pkg/issuer/acme/dns/clouddns" "github.com/jetstack-experimental/cert-manager/pkg/issuer/acme/dns/cloudflare" + "github.com/jetstack-experimental/cert-manager/pkg/issuer/acme/dns/route53" "github.com/jetstack-experimental/cert-manager/pkg/issuer/acme/dns/util" ) @@ -132,6 +133,26 @@ func (s *Solver) solverFor(crt *v1alpha1.Certificate, domain string) (solver, er if err != nil { return nil, fmt.Errorf("error instantiating cloudflare challenge solver: %s", err.Error()) } + case providerConfig.Route53 != nil: + secretAccessKeySecret, err := s.secretLister.Secrets(s.issuer.Namespace).Get(providerConfig.Route53.SecretAccessKey.Name) + if err != nil { + return nil, fmt.Errorf("error getting route53 secret access key: %s", err.Error()) + } + + secretAccessKeyBytes, ok := secretAccessKeySecret.Data[providerConfig.Cloudflare.APIKey.Key] + if !ok { + return nil, fmt.Errorf("error getting route53 secret access key: key '%s' not found in secret", providerConfig.Route53.SecretAccessKey.Key) + } + + impl, err = route53.NewDNSProviderAccessKey( + providerConfig.Route53.AccessKeyID, + string(secretAccessKeyBytes), + providerConfig.Route53.HostedZoneID, + providerConfig.Route53.Region, + ) + if err != nil { + return nil, fmt.Errorf("error instantiating route53 challenge solver: %s", err.Error()) + } default: return nil, fmt.Errorf("no dns provider config specified for domain '%s'", domain) } diff --git a/pkg/issuer/acme/dns/route53/route53.go b/pkg/issuer/acme/dns/route53/route53.go index 934f0a2d4..df42b0189 100644 --- a/pkg/issuer/acme/dns/route53/route53.go +++ b/pkg/issuer/acme/dns/route53/route53.go @@ -11,10 +11,12 @@ import ( "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/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/route53" - "github.com/xenolf/lego/acme" + + "github.com/jetstack-experimental/cert-manager/pkg/issuer/acme/dns/util" ) const ( @@ -22,7 +24,7 @@ const ( route53TTL = 10 ) -// DNSProvider implements the acme.ChallengeProvider interface +// DNSProvider implements the util.ChallengeProvider interface type DNSProvider struct { client *route53.Route53 hostedZoneID string @@ -78,16 +80,44 @@ func NewDNSProvider() (*DNSProvider, error) { }, nil } +// NewDNSProviderAccessKey returns a DNSProvider instance configured for the AWS +// Route 53 service using static credentials from its parameters +func NewDNSProviderAccessKey(accessKeyID, secretAccessKey, hostedZoneID, region string) (*DNSProvider, error) { + + creds := credentials.NewStaticCredentials(accessKeyID, secretAccessKey, "") + + r := customRetryer{} + r.NumMaxRetries = maxRetries + + config := request.WithRetryer(aws.NewConfig(), r).WithCredentials(creds) + + if region != "" { + config.WithRegion(region) + } + client := route53.New(session.New(config)) + + return &DNSProvider{ + client: client, + hostedZoneID: hostedZoneID, + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS +// propagation. Adjusting here to cope with spikes in propagation times. +func (c *DNSProvider) Timeout() (timeout, interval time.Duration) { + return 120 * time.Second, 2 * time.Second +} + // Present creates a TXT record using the specified parameters func (r *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value, _ := util.DNS01Record(domain, keyAuth) value = `"` + value + `"` return r.changeRecord("UPSERT", fqdn, value, route53TTL) } // CleanUp removes the TXT record matching the specified parameters func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value, _ := util.DNS01Record(domain, keyAuth) value = `"` + value + `"` return r.changeRecord("DELETE", fqdn, value, route53TTL) } @@ -119,7 +149,7 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { statusID := resp.ChangeInfo.Id - return acme.WaitFor(120*time.Second, 4*time.Second, func() (bool, error) { + return util.WaitFor(120*time.Second, 4*time.Second, func() (bool, error) { reqParams := &route53.GetChangeInput{ Id: statusID, } @@ -139,14 +169,14 @@ func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) { return r.hostedZoneID, nil } - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := util.FindZoneByFqdn(fqdn, util.RecursiveNameservers) if err != nil { return "", err } // .DNSName should not have a trailing dot reqParams := &route53.ListHostedZonesByNameInput{ - DNSName: aws.String(acme.UnFqdn(authZone)), + DNSName: aws.String(util.UnFqdn(authZone)), } resp, err := r.client.ListHostedZonesByName(reqParams) if err != nil { diff --git a/pkg/issuer/acme/dns/util/wait.go b/pkg/issuer/acme/dns/util/wait.go index 1f0d53a9d..c2c4a8c4e 100644 --- a/pkg/issuer/acme/dns/util/wait.go +++ b/pkg/issuer/acme/dns/util/wait.go @@ -237,3 +237,26 @@ func UnFqdn(name string) string { } return name } + +// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'. +func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error { + var lastErr string + timeup := time.After(timeout) + for { + select { + case <-timeup: + return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr) + default: + } + + stop, err := f() + if stop { + return nil + } + if err != nil { + lastErr = err.Error() + } + + time.Sleep(interval) + } +}