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 }