traefik/vendor/github.com/go-acme/lego/providers/dns/gcloud/googlecloud.go

338 lines
9.4 KiB
Go
Raw Normal View History

2019-01-07 17:30:06 +00:00
// Package gcloud implements a DNS provider for solving the DNS-01 challenge using Google Cloud DNS.
2018-05-31 07:30:04 +00:00
package gcloud
2017-02-07 21:33:23 +00:00
import (
2018-05-31 07:30:04 +00:00
"encoding/json"
2018-09-17 13:16:03 +00:00
"errors"
2017-02-07 21:33:23 +00:00
"fmt"
2017-10-31 09:42:03 +00:00
"io/ioutil"
2018-09-17 13:16:03 +00:00
"net/http"
2019-01-07 17:30:06 +00:00
"strconv"
2017-02-07 21:33:23 +00:00
"time"
2019-03-14 10:04:04 +00:00
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/log"
"github.com/go-acme/lego/platform/config/env"
"github.com/go-acme/lego/platform/wait"
2017-02-07 21:33:23 +00:00
"golang.org/x/net/context"
"golang.org/x/oauth2/google"
"google.golang.org/api/dns/v1"
2019-01-07 17:30:06 +00:00
"google.golang.org/api/googleapi"
2019-04-26 09:08:44 +00:00
"google.golang.org/api/option"
2019-01-07 17:30:06 +00:00
)
const (
changeStatusDone = "done"
2017-02-07 21:33:23 +00:00
)
2018-09-17 13:16:03 +00:00
// Config is used to configure the creation of the DNSProvider
type Config struct {
2019-01-07 17:30:06 +00:00
Debug bool
2018-09-17 13:16:03 +00:00
Project string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
2019-01-07 17:30:06 +00:00
Debug: env.GetOrDefaultBool("GCE_DEBUG", false),
TTL: env.GetOrDefaultInt("GCE_TTL", dns01.DefaultTTL),
2018-09-17 13:16:03 +00:00
PropagationTimeout: env.GetOrDefaultSecond("GCE_PROPAGATION_TIMEOUT", 180*time.Second),
PollingInterval: env.GetOrDefaultSecond("GCE_POLLING_INTERVAL", 5*time.Second),
}
}
2017-02-07 21:33:23 +00:00
// DNSProvider is an implementation of the DNSProvider interface.
type DNSProvider struct {
2018-09-17 13:16:03 +00:00
config *Config
client *dns.Service
2017-02-07 21:33:23 +00:00
}
2018-09-17 13:16:03 +00:00
// NewDNSProvider returns a DNSProvider instance configured for Google Cloud DNS.
// Project name must be passed in the environment variable: GCE_PROJECT.
2019-02-11 07:52:03 +00:00
// A Service Account can be passed in the environment variable: GCE_SERVICE_ACCOUNT
// or by specifying the keyfile location: GCE_SERVICE_ACCOUNT_FILE
2017-02-07 21:33:23 +00:00
func NewDNSProvider() (*DNSProvider, error) {
2019-01-11 15:22:03 +00:00
// Use a service account file if specified via environment variable.
2019-02-11 07:52:03 +00:00
if saKey := env.GetOrFile("GCE_SERVICE_ACCOUNT"); len(saKey) > 0 {
return NewDNSProviderServiceAccountKey([]byte(saKey))
2017-10-31 09:42:03 +00:00
}
2018-07-03 10:44:04 +00:00
2019-01-11 15:22:03 +00:00
// Use default credentials.
2019-01-25 09:24:04 +00:00
project := env.GetOrDefaultString("GCE_PROJECT", "")
2017-02-07 21:33:23 +00:00
return NewDNSProviderCredentials(project)
}
2018-09-17 13:16:03 +00:00
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Google Cloud DNS.
2017-02-07 21:33:23 +00:00
func NewDNSProviderCredentials(project string) (*DNSProvider, error) {
if project == "" {
2018-09-17 13:16:03 +00:00
return nil, fmt.Errorf("googlecloud: project name missing")
2017-02-07 21:33:23 +00:00
}
client, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)
if err != nil {
2018-09-17 13:16:03 +00:00
return nil, fmt.Errorf("googlecloud: unable to get Google Cloud client: %v", err)
2017-02-07 21:33:23 +00:00
}
2018-09-17 13:16:03 +00:00
config := NewDefaultConfig()
config.Project = project
config.HTTPClient = client
return NewDNSProviderConfig(config)
2017-02-07 21:33:23 +00:00
}
2019-02-11 07:52:03 +00:00
// NewDNSProviderServiceAccountKey uses the supplied service account JSON
2018-09-17 13:16:03 +00:00
// to return a DNSProvider instance configured for Google Cloud DNS.
2019-02-11 07:52:03 +00:00
func NewDNSProviderServiceAccountKey(saKey []byte) (*DNSProvider, error) {
if len(saKey) == 0 {
return nil, fmt.Errorf("googlecloud: Service Account is missing")
2017-10-31 09:42:03 +00:00
}
2018-05-31 07:30:04 +00:00
2019-01-11 15:22:03 +00:00
// If GCE_PROJECT is non-empty it overrides the project in the service
// account file.
2019-01-25 09:24:04 +00:00
project := env.GetOrDefaultString("GCE_PROJECT", "")
2019-01-11 15:22:03 +00:00
if project == "" {
// read project id from service account file
var datJSON struct {
ProjectID string `json:"project_id"`
}
2019-02-11 07:52:03 +00:00
err := json.Unmarshal(saKey, &datJSON)
2019-01-11 15:22:03 +00:00
if err != nil || datJSON.ProjectID == "" {
return nil, fmt.Errorf("googlecloud: project ID not found in Google Cloud Service Account file")
}
project = datJSON.ProjectID
2018-05-31 07:30:04 +00:00
}
2019-02-11 07:52:03 +00:00
conf, err := google.JWTConfigFromJSON(saKey, dns.NdevClouddnsReadwriteScope)
2017-10-31 09:42:03 +00:00
if err != nil {
2018-09-17 13:16:03 +00:00
return nil, fmt.Errorf("googlecloud: unable to acquire config: %v", err)
2017-10-31 09:42:03 +00:00
}
2018-05-31 07:30:04 +00:00
client := conf.Client(context.Background())
2017-10-31 09:42:03 +00:00
2018-09-17 13:16:03 +00:00
config := NewDefaultConfig()
config.Project = project
config.HTTPClient = client
return NewDNSProviderConfig(config)
}
2019-02-11 07:52:03 +00:00
// NewDNSProviderServiceAccount uses the supplied service account JSON file
// to return a DNSProvider instance configured for Google Cloud DNS.
func NewDNSProviderServiceAccount(saFile string) (*DNSProvider, error) {
if saFile == "" {
return nil, fmt.Errorf("googlecloud: Service Account file missing")
}
saKey, err := ioutil.ReadFile(saFile)
if err != nil {
return nil, fmt.Errorf("googlecloud: unable to read Service Account file: %v", err)
}
return NewDNSProviderServiceAccountKey(saKey)
}
2018-09-17 13:16:03 +00:00
// NewDNSProviderConfig return a DNSProvider instance configured for Google Cloud DNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("googlecloud: the configuration of the DNS provider is nil")
}
2019-04-26 09:08:44 +00:00
if config.HTTPClient == nil {
return nil, fmt.Errorf("googlecloud: unable to create Google Cloud DNS service: client is nil")
}
2018-09-17 13:16:03 +00:00
2019-04-26 09:08:44 +00:00
svc, err := dns.NewService(context.Background(), option.WithHTTPClient(config.HTTPClient))
2017-10-31 09:42:03 +00:00
if err != nil {
2018-09-17 13:16:03 +00:00
return nil, fmt.Errorf("googlecloud: unable to create Google Cloud DNS service: %v", err)
2017-10-31 09:42:03 +00:00
}
2018-09-17 13:16:03 +00:00
return &DNSProvider{config: config, client: svc}, nil
2017-10-31 09:42:03 +00:00
}
2018-10-10 14:28:04 +00:00
// Present creates a TXT record to fulfill the dns-01 challenge.
2018-07-03 10:44:04 +00:00
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
2019-01-07 17:30:06 +00:00
fqdn, value := dns01.GetRecord(domain, keyAuth)
2017-02-07 21:33:23 +00:00
2019-06-11 21:11:17 +00:00
zone, err := d.getHostedZone(fqdn)
2017-02-07 21:33:23 +00:00
if err != nil {
2018-09-17 13:16:03 +00:00
return fmt.Errorf("googlecloud: %v", err)
2017-02-07 21:33:23 +00:00
}
2018-09-21 16:38:02 +00:00
// Look for existing records.
2019-01-07 17:30:06 +00:00
existingRrSet, err := d.findTxtRecords(zone, fqdn)
2018-09-21 16:38:02 +00:00
if err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
2019-01-07 17:30:06 +00:00
for _, rrSet := range existingRrSet {
var rrd []string
for _, rr := range rrSet.Rrdatas {
data := mustUnquote(rr)
rrd = append(rrd, data)
if data == value {
log.Printf("skip: the record already exists: %s", value)
return nil
}
}
rrSet.Rrdatas = rrd
}
// Attempt to delete the existing records before adding the new one.
if len(existingRrSet) > 0 {
if err = d.applyChanges(zone, &dns.Change{Deletions: existingRrSet}); err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
}
2017-02-07 21:33:23 +00:00
rec := &dns.ResourceRecordSet{
Name: fqdn,
Rrdatas: []string{value},
2018-09-17 13:16:03 +00:00
Ttl: int64(d.config.TTL),
2017-02-07 21:33:23 +00:00
Type: "TXT",
}
2019-01-07 17:30:06 +00:00
// Append existing TXT record data to the new TXT record data
for _, rrSet := range existingRrSet {
for _, rr := range rrSet.Rrdatas {
if rr != value {
rec.Rrdatas = append(rec.Rrdatas, rr)
}
}
}
2018-09-21 16:38:02 +00:00
2019-01-07 17:30:06 +00:00
change := &dns.Change{
Additions: []*dns.ResourceRecordSet{rec},
}
2018-09-21 16:38:02 +00:00
2019-01-07 17:30:06 +00:00
if err = d.applyChanges(zone, change); err != nil {
return fmt.Errorf("googlecloud: %v", err)
2017-02-07 21:33:23 +00:00
}
2019-01-07 17:30:06 +00:00
return nil
}
func (d *DNSProvider) applyChanges(zone string, change *dns.Change) error {
if d.config.Debug {
data, _ := json.Marshal(change)
log.Printf("change (Create): %s", string(data))
}
2018-09-21 16:38:02 +00:00
2018-09-17 13:16:03 +00:00
chg, err := d.client.Changes.Create(d.config.Project, zone, change).Do()
2017-02-07 21:33:23 +00:00
if err != nil {
2019-01-07 17:30:06 +00:00
if v, ok := err.(*googleapi.Error); ok {
if v.Code == http.StatusNotFound {
return nil
}
}
data, _ := json.Marshal(change)
return fmt.Errorf("failed to perform changes [zone %s, change %s]: %v", zone, string(data), err)
2017-02-07 21:33:23 +00:00
}
2019-01-07 17:30:06 +00:00
if chg.Status == changeStatusDone {
return nil
}
chgID := chg.Id
2017-02-07 21:33:23 +00:00
// wait for change to be acknowledged
2019-01-07 17:30:06 +00:00
return wait.For("apply change", 30*time.Second, 3*time.Second, func() (bool, error) {
if d.config.Debug {
data, _ := json.Marshal(change)
log.Printf("change (Get): %s", string(data))
}
2017-02-07 21:33:23 +00:00
2019-01-07 17:30:06 +00:00
chg, err = d.client.Changes.Get(d.config.Project, zone, chgID).Do()
2017-02-07 21:33:23 +00:00
if err != nil {
2019-01-07 17:30:06 +00:00
data, _ := json.Marshal(change)
return false, fmt.Errorf("failed to get changes [zone %s, change %s]: %v", zone, string(data), err)
2017-02-07 21:33:23 +00:00
}
2019-01-07 17:30:06 +00:00
if chg.Status == changeStatusDone {
return true, nil
}
return false, fmt.Errorf("status: %s", chg.Status)
})
2017-02-07 21:33:23 +00:00
}
// CleanUp removes the TXT record matching the specified parameters.
2018-07-03 10:44:04 +00:00
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
2019-01-07 17:30:06 +00:00
fqdn, _ := dns01.GetRecord(domain, keyAuth)
2017-02-07 21:33:23 +00:00
2019-06-11 21:11:17 +00:00
zone, err := d.getHostedZone(fqdn)
2017-02-07 21:33:23 +00:00
if err != nil {
2018-09-17 13:16:03 +00:00
return fmt.Errorf("googlecloud: %v", err)
2017-02-07 21:33:23 +00:00
}
2018-07-03 10:44:04 +00:00
records, err := d.findTxtRecords(zone, fqdn)
2017-02-07 21:33:23 +00:00
if err != nil {
2018-09-17 13:16:03 +00:00
return fmt.Errorf("googlecloud: %v", err)
2017-02-07 21:33:23 +00:00
}
2018-07-23 15:30:03 +00:00
if len(records) == 0 {
return nil
2017-02-07 21:33:23 +00:00
}
2018-07-23 15:30:03 +00:00
2018-09-17 13:16:03 +00:00
_, err = d.client.Changes.Create(d.config.Project, zone, &dns.Change{Deletions: records}).Do()
2018-09-21 16:38:02 +00:00
if err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
return nil
2017-02-07 21:33:23 +00:00
}
// Timeout customizes the timeout values used by the ACME package for checking
// DNS record validity.
2018-07-03 10:44:04 +00:00
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
2018-09-17 13:16:03 +00:00
return d.config.PropagationTimeout, d.config.PollingInterval
2017-02-07 21:33:23 +00:00
}
// getHostedZone returns the managed-zone
2018-07-03 10:44:04 +00:00
func (d *DNSProvider) getHostedZone(domain string) (string, error) {
2019-01-07 17:30:06 +00:00
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
2017-02-07 21:33:23 +00:00
if err != nil {
return "", err
}
2018-07-03 10:44:04 +00:00
zones, err := d.client.ManagedZones.
2018-09-17 13:16:03 +00:00
List(d.config.Project).
2017-02-07 21:33:23 +00:00
DnsName(authZone).
Do()
if err != nil {
2018-09-17 13:16:03 +00:00
return "", fmt.Errorf("API call failed: %v", err)
2017-02-07 21:33:23 +00:00
}
if len(zones.ManagedZones) == 0 {
2018-09-17 13:16:03 +00:00
return "", fmt.Errorf("no matching domain found for domain %s", authZone)
2017-02-07 21:33:23 +00:00
}
2019-04-26 09:08:44 +00:00
for _, z := range zones.ManagedZones {
2019-06-11 21:11:17 +00:00
if z.Visibility == "public" || z.Visibility == "" {
2019-04-26 09:08:44 +00:00
return z.Name, nil
}
}
return "", fmt.Errorf("no public zone found for domain %s", authZone)
2017-02-07 21:33:23 +00:00
}
2018-07-03 10:44:04 +00:00
func (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSet, error) {
2018-09-17 13:16:03 +00:00
recs, err := d.client.ResourceRecordSets.List(d.config.Project, zone).Name(fqdn).Type("TXT").Do()
2017-02-07 21:33:23 +00:00
if err != nil {
return nil, err
}
2018-07-23 15:30:03 +00:00
return recs.Rrsets, nil
2017-02-07 21:33:23 +00:00
}
2019-01-07 17:30:06 +00:00
func mustUnquote(raw string) string {
clean, err := strconv.Unquote(raw)
if err != nil {
return raw
}
return clean
}