2016-12-17 04:10:27 +08:00
|
|
|
package google
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2017-01-04 01:24:00 +08:00
|
|
|
"strings"
|
2016-12-17 04:10:27 +08:00
|
|
|
|
|
|
|
gauth "golang.org/x/oauth2/google"
|
2018-03-01 23:20:34 +08:00
|
|
|
gdns "google.golang.org/api/dns/v1"
|
2017-01-04 01:24:00 +08:00
|
|
|
|
2020-01-28 23:42:31 +08:00
|
|
|
"github.com/StackExchange/dnscontrol/v2/models"
|
|
|
|
"github.com/StackExchange/dnscontrol/v2/providers"
|
|
|
|
"github.com/StackExchange/dnscontrol/v2/providers/diff"
|
2016-12-17 04:10:27 +08:00
|
|
|
)
|
|
|
|
|
2018-01-05 08:19:35 +08:00
|
|
|
var features = providers.DocumentationNotes{
|
2017-09-15 04:13:17 +08:00
|
|
|
providers.DocCreateDomains: providers.Can(),
|
2018-01-05 08:19:35 +08:00
|
|
|
providers.DocDualHost: providers.Can(),
|
2017-09-15 04:13:17 +08:00
|
|
|
providers.DocOfficiallySupported: providers.Can(),
|
2018-01-05 08:19:35 +08:00
|
|
|
providers.CanUsePTR: providers.Can(),
|
|
|
|
providers.CanUseSRV: providers.Can(),
|
|
|
|
providers.CanUseCAA: providers.Can(),
|
2018-10-14 22:53:11 +08:00
|
|
|
providers.CanUseTXTMulti: providers.Can(),
|
2020-02-22 02:49:10 +08:00
|
|
|
providers.CanGetZones: providers.Can(),
|
2017-09-15 04:13:17 +08:00
|
|
|
}
|
|
|
|
|
2019-02-20 01:30:39 +08:00
|
|
|
func sPtr(s string) *string {
|
|
|
|
return &s
|
|
|
|
}
|
|
|
|
|
2016-12-17 04:10:27 +08:00
|
|
|
func init() {
|
2018-01-05 08:19:35 +08:00
|
|
|
providers.RegisterDomainServiceProviderType("GCLOUD", New, features)
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
type gcloud struct {
|
2019-02-20 01:30:39 +08:00
|
|
|
client *gdns.Service
|
|
|
|
project string
|
|
|
|
nameServerSet *string
|
|
|
|
zones map[string]*gdns.ManagedZone
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
|
|
|
|
2020-02-22 02:49:10 +08:00
|
|
|
type errNoExist struct {
|
|
|
|
domain string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e errNoExist) Error() string {
|
|
|
|
return fmt.Sprintf("Domain '%s' not found in gcloud account", e.domain)
|
|
|
|
}
|
|
|
|
|
2017-01-04 01:24:00 +08:00
|
|
|
// New creates a new gcloud provider
|
2019-02-20 01:30:39 +08:00
|
|
|
func New(cfg map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
2019-07-03 00:32:54 +08:00
|
|
|
// the key as downloaded is json encoded with literal "\n" instead of newlines.
|
|
|
|
// in some cases (round-tripping through env vars) this tends to get messed up.
|
|
|
|
// fix it if we find that.
|
|
|
|
if key, ok := cfg["private_key"]; ok {
|
|
|
|
cfg["private_key"] = strings.Replace(key, "\\n", "\n", -1)
|
|
|
|
}
|
2017-01-12 03:09:14 +08:00
|
|
|
raw, err := json.Marshal(cfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
2017-01-12 03:09:14 +08:00
|
|
|
config, err := gauth.JWTConfigFromJSON(raw, "https://www.googleapis.com/auth/ndev.clouddns.readwrite")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
2017-01-12 03:09:14 +08:00
|
|
|
ctx := context.Background()
|
|
|
|
hc := config.Client(ctx)
|
2018-03-01 23:20:34 +08:00
|
|
|
dcli, err := gdns.New(hc)
|
2016-12-17 04:10:27 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-02-20 01:30:39 +08:00
|
|
|
var nss *string = nil
|
|
|
|
if val, ok := cfg["name_server_set"]; ok {
|
|
|
|
fmt.Printf("GCLOUD :name_server_set %s configured\n", val)
|
|
|
|
nss = sPtr(val)
|
|
|
|
}
|
2020-02-22 02:49:10 +08:00
|
|
|
|
|
|
|
g := &gcloud{
|
2019-02-20 01:30:39 +08:00
|
|
|
client: dcli,
|
|
|
|
nameServerSet: nss,
|
|
|
|
project: cfg["project_id"],
|
2020-02-22 02:49:10 +08:00
|
|
|
}
|
|
|
|
return g, g.loadZoneInfo()
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
|
|
|
|
2020-02-22 02:49:10 +08:00
|
|
|
func (g *gcloud) loadZoneInfo() error {
|
|
|
|
if g.zones != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
g.zones = map[string]*gdns.ManagedZone{}
|
|
|
|
pageToken := ""
|
|
|
|
for {
|
|
|
|
resp, err := g.client.ManagedZones.List(g.project).PageToken(pageToken).Do()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, z := range resp.ManagedZones {
|
|
|
|
g.zones[z.DnsName] = z
|
|
|
|
}
|
|
|
|
if pageToken = resp.NextPageToken; pageToken == "" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2017-01-04 01:24:00 +08:00
|
|
|
}
|
|
|
|
|
2020-02-22 02:49:10 +08:00
|
|
|
// ListZones returns the list of zones (domains) in this account.
|
|
|
|
func (g *gcloud) ListZones() ([]string, error) {
|
|
|
|
var zones []string
|
|
|
|
for i, _ := range g.zones {
|
|
|
|
zones = append(zones, strings.TrimSuffix(i, "."))
|
|
|
|
}
|
|
|
|
return zones, nil
|
2017-01-04 01:24:00 +08:00
|
|
|
}
|
|
|
|
|
2018-03-01 23:20:34 +08:00
|
|
|
func (g *gcloud) getZone(domain string) (*gdns.ManagedZone, error) {
|
2016-12-17 04:10:27 +08:00
|
|
|
return g.zones[domain+"."], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *gcloud) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
|
|
|
zone, err := g.getZone(domain)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return models.StringsToNameservers(zone.NameServers), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type key struct {
|
|
|
|
Type string
|
|
|
|
Name string
|
|
|
|
}
|
|
|
|
|
2018-03-01 23:20:34 +08:00
|
|
|
func keyFor(r *gdns.ResourceRecordSet) key {
|
2016-12-17 04:10:27 +08:00
|
|
|
return key{Type: r.Type, Name: r.Name}
|
|
|
|
}
|
|
|
|
func keyForRec(r *models.RecordConfig) key {
|
2018-03-20 05:18:58 +08:00
|
|
|
return key{Type: r.Type, Name: r.GetLabelFQDN() + "."}
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
|
|
|
|
2020-02-18 21:59:18 +08:00
|
|
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
2020-02-22 02:49:10 +08:00
|
|
|
func (g *gcloud) GetZoneRecords(domain string) (models.Records, error) {
|
|
|
|
existingRecords, _, _, err := g.getZoneSets(domain)
|
|
|
|
return existingRecords, err
|
2020-02-18 21:59:18 +08:00
|
|
|
}
|
|
|
|
|
2020-02-22 02:49:10 +08:00
|
|
|
func (g *gcloud) getZoneSets(domain string) (models.Records, map[key]*gdns.ResourceRecordSet, string, error) {
|
|
|
|
rrs, zoneName, err := g.getRecords(domain)
|
2016-12-17 04:10:27 +08:00
|
|
|
if err != nil {
|
2020-02-22 02:49:10 +08:00
|
|
|
return nil, nil, "", err
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
2018-01-10 01:53:16 +08:00
|
|
|
// convert to dnscontrol RecordConfig format
|
2017-01-12 03:38:07 +08:00
|
|
|
existingRecords := []*models.RecordConfig{}
|
2018-03-01 23:20:34 +08:00
|
|
|
oldRRs := map[key]*gdns.ResourceRecordSet{}
|
2016-12-17 04:10:27 +08:00
|
|
|
for _, set := range rrs {
|
|
|
|
oldRRs[keyFor(set)] = set
|
|
|
|
for _, rec := range set.Rrdatas {
|
2020-02-22 02:49:10 +08:00
|
|
|
existingRecords = append(existingRecords, nativeToRecord(set, rec, domain))
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
|
|
|
}
|
2020-02-22 02:49:10 +08:00
|
|
|
return existingRecords, oldRRs, zoneName, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
|
|
if err := dc.Punycode(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
existingRecords, oldRRs, zoneName, err := g.getZoneSets(dc.Name)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-12-17 04:10:27 +08:00
|
|
|
|
2017-11-08 06:12:17 +08:00
|
|
|
// Normalize
|
2018-01-05 08:19:35 +08:00
|
|
|
models.PostProcessRecords(existingRecords)
|
2017-11-08 06:12:17 +08:00
|
|
|
|
2016-12-17 04:10:27 +08:00
|
|
|
// first collect keys that have changed
|
2017-01-12 03:38:07 +08:00
|
|
|
differ := diff.New(dc)
|
|
|
|
_, create, delete, modify := differ.IncrementalDiff(existingRecords)
|
2016-12-17 04:10:27 +08:00
|
|
|
changedKeys := map[key]bool{}
|
|
|
|
desc := ""
|
|
|
|
for _, c := range create {
|
|
|
|
desc += fmt.Sprintln(c)
|
2017-01-12 03:38:07 +08:00
|
|
|
changedKeys[keyForRec(c.Desired)] = true
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
|
|
|
for _, d := range delete {
|
|
|
|
desc += fmt.Sprintln(d)
|
2017-01-12 03:38:07 +08:00
|
|
|
changedKeys[keyForRec(d.Existing)] = true
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
|
|
|
for _, m := range modify {
|
|
|
|
desc += fmt.Sprintln(m)
|
2017-01-12 03:38:07 +08:00
|
|
|
changedKeys[keyForRec(m.Existing)] = true
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
|
|
|
if len(changedKeys) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2018-03-01 23:20:34 +08:00
|
|
|
chg := &gdns.Change{Kind: "dns#change"}
|
2016-12-17 04:10:27 +08:00
|
|
|
for ck := range changedKeys {
|
|
|
|
// remove old version (if present)
|
|
|
|
if old, ok := oldRRs[ck]; ok {
|
|
|
|
chg.Deletions = append(chg.Deletions, old)
|
|
|
|
}
|
2018-01-10 01:53:16 +08:00
|
|
|
// collect records to replace with
|
2018-03-01 23:20:34 +08:00
|
|
|
newRRs := &gdns.ResourceRecordSet{
|
2016-12-17 04:10:27 +08:00
|
|
|
Name: ck.Name,
|
|
|
|
Type: ck.Type,
|
|
|
|
Kind: "dns#resourceRecordSet",
|
|
|
|
}
|
|
|
|
for _, r := range dc.Records {
|
|
|
|
if keyForRec(r) == ck {
|
2018-02-16 01:02:50 +08:00
|
|
|
newRRs.Rrdatas = append(newRRs.Rrdatas, r.GetTargetCombined())
|
2016-12-17 04:10:27 +08:00
|
|
|
newRRs.Ttl = int64(r.TTL)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(newRRs.Rrdatas) > 0 {
|
|
|
|
chg.Additions = append(chg.Additions, newRRs)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
runChange := func() error {
|
|
|
|
_, err := g.client.Changes.Create(g.project, zoneName, chg).Do()
|
|
|
|
return err
|
|
|
|
}
|
2017-01-04 04:26:08 +08:00
|
|
|
return []*models.Correction{{
|
2017-01-04 01:24:00 +08:00
|
|
|
Msg: desc,
|
|
|
|
F: runChange,
|
2017-01-04 04:26:08 +08:00
|
|
|
}}, nil
|
2016-12-17 04:10:27 +08:00
|
|
|
}
|
|
|
|
|
2018-03-01 23:20:34 +08:00
|
|
|
func nativeToRecord(set *gdns.ResourceRecordSet, rec, origin string) *models.RecordConfig {
|
2018-02-16 01:02:50 +08:00
|
|
|
r := &models.RecordConfig{}
|
|
|
|
r.SetLabelFromFQDN(set.Name, origin)
|
|
|
|
r.TTL = uint32(set.Ttl)
|
|
|
|
if err := r.PopulateFromString(set.Type, rec, origin); err != nil {
|
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-29 00:06:56 +08:00
|
|
|
panic(fmt.Errorf("unparsable record received from GCLOUD: %w", err))
|
2018-02-16 01:02:50 +08:00
|
|
|
}
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2018-03-01 23:20:34 +08:00
|
|
|
func (g *gcloud) getRecords(domain string) ([]*gdns.ResourceRecordSet, string, error) {
|
2016-12-17 04:10:27 +08:00
|
|
|
zone, err := g.getZone(domain)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
pageToken := ""
|
2018-03-01 23:20:34 +08:00
|
|
|
sets := []*gdns.ResourceRecordSet{}
|
2016-12-17 04:10:27 +08:00
|
|
|
for {
|
|
|
|
call := g.client.ResourceRecordSets.List(g.project, zone.Name)
|
|
|
|
if pageToken != "" {
|
|
|
|
call = call.PageToken(pageToken)
|
|
|
|
}
|
|
|
|
resp, err := call.Do()
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
for _, rrs := range resp.Rrsets {
|
|
|
|
if rrs.Type == "SOA" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
sets = append(sets, rrs)
|
|
|
|
}
|
|
|
|
if pageToken = resp.NextPageToken; pageToken == "" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return sets, zone.Name, nil
|
|
|
|
}
|
2017-01-04 01:24:00 +08:00
|
|
|
|
2017-01-04 04:26:08 +08:00
|
|
|
func (g *gcloud) EnsureDomainExists(domain string) error {
|
|
|
|
z, err := g.getZone(domain)
|
|
|
|
if err != nil {
|
|
|
|
if _, ok := err.(errNoExist); !ok {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if z != nil {
|
|
|
|
return nil
|
|
|
|
}
|
2019-02-20 01:30:39 +08:00
|
|
|
var mz *gdns.ManagedZone
|
|
|
|
if g.nameServerSet != nil {
|
|
|
|
fmt.Printf("Adding zone for %s to gcloud account with name_server_set %s\n", domain, *g.nameServerSet)
|
|
|
|
mz = &gdns.ManagedZone{
|
|
|
|
DnsName: domain + ".",
|
|
|
|
NameServerSet: *g.nameServerSet,
|
|
|
|
Name: "zone-" + strings.Replace(domain, ".", "-", -1),
|
|
|
|
Description: "zone added by dnscontrol",
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fmt.Printf("Adding zone for %s to gcloud account \n", domain)
|
|
|
|
mz = &gdns.ManagedZone{
|
|
|
|
DnsName: domain + ".",
|
|
|
|
Name: "zone-" + strings.Replace(domain, ".", "-", -1),
|
|
|
|
Description: "zone added by dnscontrol",
|
|
|
|
}
|
2017-01-04 04:26:08 +08:00
|
|
|
}
|
2018-01-10 01:53:16 +08:00
|
|
|
g.zones = nil // reset cache
|
2017-01-04 04:26:08 +08:00
|
|
|
_, err = g.client.ManagedZones.Create(g.project, mz).Do()
|
|
|
|
return err
|
2017-01-04 01:24:00 +08:00
|
|
|
}
|