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