github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/google/google.go (about)

     1  package google
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"strings"
     8  
     9  	gauth "golang.org/x/oauth2/google"
    10  	"google.golang.org/api/dns/v1"
    11  
    12  	"github.com/StackExchange/dnscontrol/models"
    13  	"github.com/StackExchange/dnscontrol/providers"
    14  	"github.com/StackExchange/dnscontrol/providers/diff"
    15  )
    16  
    17  var docNotes = providers.DocumentationNotes{
    18  	providers.DocDualHost:            providers.Can(),
    19  	providers.DocCreateDomains:       providers.Can(),
    20  	providers.DocOfficiallySupported: providers.Can(),
    21  }
    22  
    23  func init() {
    24  	providers.RegisterDomainServiceProviderType("GCLOUD", New, providers.CanUsePTR, providers.CanUseSRV, providers.CanUseCAA, docNotes)
    25  }
    26  
    27  type gcloud struct {
    28  	client  *dns.Service
    29  	project string
    30  	zones   map[string]*dns.ManagedZone
    31  }
    32  
    33  // New creates a new gcloud provider
    34  func New(cfg map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
    35  	raw, err := json.Marshal(cfg)
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  	config, err := gauth.JWTConfigFromJSON(raw, "https://www.googleapis.com/auth/ndev.clouddns.readwrite")
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  	ctx := context.Background()
    44  	hc := config.Client(ctx)
    45  	dcli, err := dns.New(hc)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  	return &gcloud{
    50  		client:  dcli,
    51  		project: cfg["project_id"],
    52  	}, nil
    53  }
    54  
    55  type errNoExist struct {
    56  	domain string
    57  }
    58  
    59  func (e errNoExist) Error() string {
    60  	return fmt.Sprintf("Domain %s not found in gcloud account", e.domain)
    61  }
    62  
    63  func (g *gcloud) getZone(domain string) (*dns.ManagedZone, error) {
    64  	if g.zones == nil {
    65  		g.zones = map[string]*dns.ManagedZone{}
    66  		pageToken := ""
    67  		for {
    68  			resp, err := g.client.ManagedZones.List(g.project).PageToken(pageToken).Do()
    69  			if err != nil {
    70  				return nil, err
    71  			}
    72  			for _, z := range resp.ManagedZones {
    73  				g.zones[z.DnsName] = z
    74  			}
    75  			if pageToken = resp.NextPageToken; pageToken == "" {
    76  				break
    77  			}
    78  		}
    79  	}
    80  	if g.zones[domain+"."] == nil {
    81  		return nil, errNoExist{domain}
    82  	}
    83  	return g.zones[domain+"."], nil
    84  }
    85  
    86  func (g *gcloud) GetNameservers(domain string) ([]*models.Nameserver, error) {
    87  	zone, err := g.getZone(domain)
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  	return models.StringsToNameservers(zone.NameServers), nil
    92  }
    93  
    94  type key struct {
    95  	Type string
    96  	Name string
    97  }
    98  
    99  func keyFor(r *dns.ResourceRecordSet) key {
   100  	return key{Type: r.Type, Name: r.Name}
   101  }
   102  func keyForRec(r *models.RecordConfig) key {
   103  	return key{Type: r.Type, Name: r.NameFQDN + "."}
   104  }
   105  
   106  func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
   107  	if err := dc.Punycode(); err != nil {
   108  		return nil, err
   109  	}
   110  	rrs, zoneName, err := g.getRecords(dc.Name)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	//convert to dnscontrol RecordConfig format
   115  	existingRecords := []*models.RecordConfig{}
   116  	oldRRs := map[key]*dns.ResourceRecordSet{}
   117  	for _, set := range rrs {
   118  		nameWithoutDot := set.Name
   119  		if strings.HasSuffix(nameWithoutDot, ".") {
   120  			nameWithoutDot = nameWithoutDot[:len(nameWithoutDot)-1]
   121  		}
   122  		oldRRs[keyFor(set)] = set
   123  		for _, rec := range set.Rrdatas {
   124  			r := &models.RecordConfig{
   125  				NameFQDN:       nameWithoutDot,
   126  				Type:           set.Type,
   127  				Target:         rec,
   128  				TTL:            uint32(set.Ttl),
   129  				CombinedTarget: true,
   130  			}
   131  			existingRecords = append(existingRecords, r)
   132  		}
   133  	}
   134  
   135  	for _, want := range dc.Records {
   136  		want.MergeToTarget()
   137  	}
   138  
   139  	// first collect keys that have changed
   140  	differ := diff.New(dc)
   141  	_, create, delete, modify := differ.IncrementalDiff(existingRecords)
   142  	changedKeys := map[key]bool{}
   143  	desc := ""
   144  	for _, c := range create {
   145  		desc += fmt.Sprintln(c)
   146  		changedKeys[keyForRec(c.Desired)] = true
   147  	}
   148  	for _, d := range delete {
   149  		desc += fmt.Sprintln(d)
   150  		changedKeys[keyForRec(d.Existing)] = true
   151  	}
   152  	for _, m := range modify {
   153  		desc += fmt.Sprintln(m)
   154  		changedKeys[keyForRec(m.Existing)] = true
   155  	}
   156  	if len(changedKeys) == 0 {
   157  		return nil, nil
   158  	}
   159  	chg := &dns.Change{Kind: "dns#change"}
   160  	for ck := range changedKeys {
   161  		// remove old version (if present)
   162  		if old, ok := oldRRs[ck]; ok {
   163  			chg.Deletions = append(chg.Deletions, old)
   164  		}
   165  		//collect records to replace with
   166  		newRRs := &dns.ResourceRecordSet{
   167  			Name: ck.Name,
   168  			Type: ck.Type,
   169  			Kind: "dns#resourceRecordSet",
   170  		}
   171  		for _, r := range dc.Records {
   172  			if keyForRec(r) == ck {
   173  				newRRs.Rrdatas = append(newRRs.Rrdatas, r.Target)
   174  				newRRs.Ttl = int64(r.TTL)
   175  			}
   176  		}
   177  		if len(newRRs.Rrdatas) > 0 {
   178  			chg.Additions = append(chg.Additions, newRRs)
   179  		}
   180  	}
   181  
   182  	runChange := func() error {
   183  		_, err := g.client.Changes.Create(g.project, zoneName, chg).Do()
   184  		return err
   185  	}
   186  	return []*models.Correction{{
   187  		Msg: desc,
   188  		F:   runChange,
   189  	}}, nil
   190  }
   191  
   192  func (g *gcloud) getRecords(domain string) ([]*dns.ResourceRecordSet, string, error) {
   193  	zone, err := g.getZone(domain)
   194  	if err != nil {
   195  		return nil, "", err
   196  	}
   197  	pageToken := ""
   198  	sets := []*dns.ResourceRecordSet{}
   199  	for {
   200  		call := g.client.ResourceRecordSets.List(g.project, zone.Name)
   201  		if pageToken != "" {
   202  			call = call.PageToken(pageToken)
   203  		}
   204  		resp, err := call.Do()
   205  		if err != nil {
   206  			return nil, "", err
   207  		}
   208  		for _, rrs := range resp.Rrsets {
   209  			if rrs.Type == "SOA" {
   210  				continue
   211  			}
   212  			sets = append(sets, rrs)
   213  		}
   214  		if pageToken = resp.NextPageToken; pageToken == "" {
   215  			break
   216  		}
   217  	}
   218  	return sets, zone.Name, nil
   219  }
   220  
   221  func (g *gcloud) EnsureDomainExists(domain string) error {
   222  	z, err := g.getZone(domain)
   223  	if err != nil {
   224  		if _, ok := err.(errNoExist); !ok {
   225  			return err
   226  		}
   227  	}
   228  	if z != nil {
   229  		return nil
   230  	}
   231  	fmt.Printf("Adding zone for %s to gcloud account\n", domain)
   232  	mz := &dns.ManagedZone{
   233  		DnsName:     domain + ".",
   234  		Name:        strings.Replace(domain, ".", "-", -1),
   235  		Description: "zone added by dnscontrol",
   236  	}
   237  	g.zones = nil //reset cache
   238  	_, err = g.client.ManagedZones.Create(g.project, mz).Do()
   239  	return err
   240  }