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:
parent
9ba0eda945
commit
4da4506839
4 changed files with 240 additions and 0 deletions
1
cli.go
1
cli.go
|
@ -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")
|
||||
|
|
|
@ -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":
|
||||
|
|
151
providers/dns/googlecloud/dns_challenge_googlecloud.go
Normal file
151
providers/dns/googlecloud/dns_challenge_googlecloud.go
Normal 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")
|
||||
}
|
85
providers/dns/googlecloud/dns_challenge_googlecloud_test.go
Normal file
85
providers/dns/googlecloud/dns_challenge_googlecloud_test.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue