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 }