Add DNS challenge provider for Google Cloud DNS

Use GCE_PROJECT to designate your GCE project.

Authentication is automatically picked up from gcloud credentials if
running locally and from GCE metadata if run within Google Cloud.

Requires at least permission scope
"https://www.googleapis.com/auth/ndev.clouddns.readwrite"
This commit is contained in:
Peter Waldschmidt 2016-03-18 11:22:33 -04:00
parent 9ba0eda945
commit 4da4506839
4 changed files with 240 additions and 0 deletions

1
cli.go
View file

@ -163,6 +163,7 @@ Here is an example bash command using the CloudFlare DNS provider:
fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN")
fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_API_KEY")
fmt.Fprintln(w, "\tgandi:\tGANDI_API_KEY")
fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT")
fmt.Fprintln(w, "\tmanual:\tnone")
fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_API_USER, NAMECHEAP_API_KEY")
fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER")

View file

@ -15,6 +15,7 @@ import (
"github.com/xenolf/lego/providers/dns/digitalocean"
"github.com/xenolf/lego/providers/dns/dnsimple"
"github.com/xenolf/lego/providers/dns/gandi"
"github.com/xenolf/lego/providers/dns/googlecloud"
"github.com/xenolf/lego/providers/dns/namecheap"
"github.com/xenolf/lego/providers/dns/rfc2136"
"github.com/xenolf/lego/providers/dns/route53"
@ -97,6 +98,8 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
case "gandi":
apiKey := os.Getenv("GANDI_API_KEY")
provider, err = gandi.NewDNSProvider(apiKey)
case "gcloud":
provider, err = googleclouddns.NewDNSProvider("")
case "namecheap":
provider, err = namecheap.NewDNSProvider("", "")
case "route53":

View file

@ -0,0 +1,151 @@
package googleclouddns
import (
"fmt"
"os"
"strings"
"time"
"github.com/xenolf/lego/acme"
"golang.org/x/net/context"
"golang.org/x/oauth2/google"
"google.golang.org/api/dns/v1"
)
// DNSProvider is an implementation of the DNSProvider interface.
type DNSProvider struct {
project string
client *dns.Service
}
// NewDNSProvider returns a DNSProvider instance with a configured gcloud client.
// Authentication is done using the local account credentials managed by the gcloud utility.
func NewDNSProvider(project string) (*DNSProvider, error) {
if project == "" {
project = gcloudEnvAuth()
}
if project == "" {
return nil, fmt.Errorf("Google Cloud project name missing")
}
client, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)
if err != nil {
return nil, fmt.Errorf("Unable to get Google Cloud client: %v", err)
}
svc, err := dns.New(client)
if err != nil {
return nil, fmt.Errorf("Unable to create Google Cloud DNS service: %v", err)
}
return &DNSProvider{
project: project,
client: svc,
}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (c *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
zone, err := c.getHostedZone(domain)
if err != nil {
return err
}
rec := &dns.ResourceRecordSet{
Name: fqdn,
Rrdatas: []string{value},
Ttl: int64(ttl),
Type: "TXT",
}
change := &dns.Change{
Additions: []*dns.ResourceRecordSet{rec},
}
chg, err := c.client.Changes.Create(c.project, zone, change).Do()
if err != nil {
return err
}
// wait for change to be acknowledged
for chg.Status == "pending" {
time.Sleep(time.Second)
chg, err = c.client.Changes.Get(c.project, zone, chg.Id).Do()
if err != nil {
return err
}
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
zone, err := c.getHostedZone(domain)
if err != nil {
return err
}
records, err := c.findTxtRecords(zone, fqdn)
if err != nil {
return err
}
for _, rec := range records {
change := &dns.Change{
Deletions: []*dns.ResourceRecordSet{rec},
}
_, err = c.client.Changes.Create(c.project, zone, change).Do()
if err != nil {
return err
}
}
return nil
}
func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 180 * time.Second, 5 * time.Second
}
// getHostedZone returns the managed-zone
func (c *DNSProvider) getHostedZone(domain string) (string, error) {
zones, err := c.client.ManagedZones.List(c.project).Do()
if err != nil {
return "", fmt.Errorf("GoogleCloud API call failed: %v", err)
}
for _, z := range zones.ManagedZones {
if strings.HasSuffix(domain+".", z.DnsName) {
return z.Name, nil
}
}
return "", fmt.Errorf("No matching GoogleCloud domain found for domain %s", domain)
}
func (c *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSet, error) {
recs, err := c.client.ResourceRecordSets.List(c.project, zone).Do()
if err != nil {
return nil, err
}
found := []*dns.ResourceRecordSet{}
for _, r := range recs.Rrsets {
if r.Type == "TXT" && r.Name == fqdn {
found = append(found, r)
}
}
return found, nil
}
func gcloudEnvAuth() (gcloud string) {
return os.Getenv("GCE_PROJECT")
}

View file

@ -0,0 +1,85 @@
package googleclouddns
import (
"os"
"testing"
"time"
"golang.org/x/net/context"
"golang.org/x/oauth2/google"
"google.golang.org/api/dns/v1"
"github.com/stretchr/testify/assert"
)
var (
gcloudLiveTest bool
gcloudProject string
gcloudDomain string
)
func init() {
gcloudProject = os.Getenv("GCE_PROJECT")
gcloudDomain = os.Getenv("GCE_DOMAIN")
_, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)
if err == nil && len(gcloudProject) > 0 && len(gcloudDomain) > 0 {
gcloudLiveTest = true
}
}
func restoreGCloudEnv() {
os.Setenv("GCE_PROJECT", gcloudProject)
}
func TestNewDNSProviderValid(t *testing.T) {
if !gcloudLiveTest {
t.Skip("skipping live test (requires credentials)")
}
os.Setenv("GCE_PROJECT", "")
_, err := NewDNSProvider("my-project")
assert.NoError(t, err)
restoreGCloudEnv()
}
func TestNewDNSProviderValidEnv(t *testing.T) {
if !gcloudLiveTest {
t.Skip("skipping live test (requires credentials)")
}
os.Setenv("GCE_PROJECT", "my-project")
_, err := NewDNSProvider("")
assert.NoError(t, err)
restoreGCloudEnv()
}
func TestNewDNSProviderMissingCredErr(t *testing.T) {
os.Setenv("GCE_PROJECT", "")
_, err := NewDNSProvider("")
assert.EqualError(t, err, "Google Cloud project name missing")
restoreGCloudEnv()
}
func TestLiveGoogleCloudPresent(t *testing.T) {
if !gcloudLiveTest {
t.Skip("skipping live test")
}
provider, err := NewDNSProvider(gcloudProject)
assert.NoError(t, err)
err = provider.Present(gcloudDomain, "", "123d==")
assert.NoError(t, err)
}
func TestLiveGoogleCloudCleanUp(t *testing.T) {
if !gcloudLiveTest {
t.Skip("skipping live test")
}
time.Sleep(time.Second * 1)
provider, err := NewDNSProvider(gcloudProject)
assert.NoError(t, err)
err = provider.CleanUp(gcloudDomain, "", "123d==")
assert.NoError(t, err)
}