github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/gandi_v5/gandi_v5Provider.go (about) 1 package gandi5 2 3 /* 4 5 Gandi API V5 LiveDNS provider: 6 7 Documentation: https://api.gandi.net/docs/ 8 Endpoint: https://api.gandi.net/ 9 10 Settings from `creds.json`: 11 - apikey 12 - sharing_id (optional) 13 14 */ 15 16 import ( 17 "encoding/json" 18 "fmt" 19 "os" 20 "sort" 21 "strconv" 22 "strings" 23 24 "github.com/miekg/dns/dnsutil" 25 gandi "github.com/tiramiseb/go-gandi" 26 27 "github.com/StackExchange/dnscontrol/v2/models" 28 "github.com/StackExchange/dnscontrol/v2/pkg/printer" 29 "github.com/StackExchange/dnscontrol/v2/providers" 30 "github.com/StackExchange/dnscontrol/v2/providers/diff" 31 ) 32 33 // Section 1: Register this provider in the system. 34 35 // init registers the provider to dnscontrol. 36 func init() { 37 providers.RegisterDomainServiceProviderType("GANDI_V5", newDsp, features) 38 providers.RegisterRegistrarType("GANDI_V5", newReg) 39 } 40 41 // features declares which features and options are available. 42 var features = providers.DocumentationNotes{ 43 providers.CanUseCAA: providers.Can(), 44 providers.CanUsePTR: providers.Can(), 45 providers.CanUseSRV: providers.Can(), 46 providers.CantUseNOPURGE: providers.Cannot(), 47 providers.DocCreateDomains: providers.Cannot("Can only manage domains registered through their service"), 48 providers.DocOfficiallySupported: providers.Cannot(), 49 providers.CanGetZones: providers.Can(), 50 } 51 52 // Section 2: Define the API client. 53 54 // api is the api handle used to store any client-related state. 55 type api struct { 56 apikey string 57 sharingid string 58 debug bool 59 } 60 61 // newDsp generates a DNS Service Provider client handle. 62 func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 63 return newHelper(conf, metadata) 64 } 65 66 // newReg generates a Registrar Provider client handle. 67 func newReg(conf map[string]string) (providers.Registrar, error) { 68 return newHelper(conf, nil) 69 } 70 71 // newHelper generates a handle. 72 func newHelper(m map[string]string, metadata json.RawMessage) (*api, error) { 73 api := &api{} 74 api.apikey = m["apikey"] 75 if api.apikey == "" { 76 return nil, fmt.Errorf("missing Gandi apikey") 77 } 78 api.sharingid = m["sharing_id"] 79 debug, err := strconv.ParseBool(os.Getenv("GANDI_V5_DEBUG")) 80 if err == nil { 81 api.debug = debug 82 } 83 84 return api, nil 85 } 86 87 // Section 3: Domain Service Provider (DSP) related functions 88 89 // NB(tal): To future-proof your code, all new providers should 90 // implement GetDomainCorrections exactly as you see here 91 // (byte-for-byte the same). In 3.0 92 // we plan on using just the individual calls to GetZoneRecords, 93 // PostProcessRecords, and so on. 94 // 95 // Currently every provider does things differently, which prevents 96 // us from doing things like using GetZoneRecords() of a provider 97 // to make convertzone work with all providers. 98 99 // GetDomainCorrections get the current and existing records, 100 // post-process them, and generate corrections. 101 func (client *api) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 102 existing, err := client.GetZoneRecords(dc.Name) 103 if err != nil { 104 return nil, err 105 } 106 models.PostProcessRecords(existing) 107 clean := PrepFoundRecords(existing) 108 PrepDesiredRecords(dc) 109 return client.GenerateDomainCorrections(dc, clean) 110 } 111 112 // GetZoneRecords gathers the DNS records and converts them to 113 // dnscontrol's format. 114 func (client *api) GetZoneRecords(domain string) (models.Records, error) { 115 g := gandi.NewLiveDNSClient(client.apikey, gandi.Config{SharingID: client.sharingid, Debug: client.debug}) 116 117 // Get all the existing records: 118 records, err := g.ListDomainRecords(domain) 119 if err != nil { 120 return nil, err 121 } 122 123 // Convert them to DNScontrol's native format: 124 existingRecords := []*models.RecordConfig{} 125 for _, rr := range records { 126 existingRecords = append(existingRecords, nativeToRecords(rr, domain)...) 127 } 128 129 return existingRecords, nil 130 } 131 132 // PrepFoundRecords munges any records to make them compatible with 133 // this provider. Usually this is a no-op. 134 func PrepFoundRecords(recs models.Records) models.Records { 135 // If there are records that need to be modified, removed, etc. we 136 // do it here. Usually this is a no-op. 137 return recs 138 } 139 140 // PrepDesiredRecords munges any records to best suit this provider. 141 func PrepDesiredRecords(dc *models.DomainConfig) { 142 // Sort through the dc.Records, eliminate any that can't be 143 // supported; modify any that need adjustments to work with the 144 // provider. We try to do minimal changes otherwise it gets 145 // confusing. 146 147 dc.Punycode() 148 149 recordsToKeep := make([]*models.RecordConfig, 0, len(dc.Records)) 150 for _, rec := range dc.Records { 151 if rec.TTL < 300 { 152 printer.Warnf("Gandi does not support ttls < 300. Setting %s from %d to 300\n", rec.GetLabelFQDN(), rec.TTL) 153 rec.TTL = 300 154 } 155 if rec.TTL > 2592000 { 156 printer.Warnf("Gandi does not support ttls > 30 days. Setting %s from %d to 2592000\n", rec.GetLabelFQDN(), rec.TTL) 157 rec.TTL = 2592000 158 } 159 if rec.Type == "TXT" { 160 rec.SetTarget("\"" + rec.GetTargetField() + "\"") // FIXME(tlim): Should do proper quoting. 161 } 162 if rec.Type == "NS" && rec.GetLabel() == "@" { 163 if !strings.HasSuffix(rec.GetTargetField(), ".gandi.net.") { 164 printer.Warnf("Gandi does not support changing apex NS records. Ignoring %s\n", rec.GetTargetField()) 165 } 166 continue 167 } 168 recordsToKeep = append(recordsToKeep, rec) 169 } 170 dc.Records = recordsToKeep 171 } 172 173 // GenerateDomainCorrections takes the desired and existing records 174 // and produces a Correction list. The correction list is simply 175 // a list of functions to call to actually make the desired 176 // correction, and a message to output to the user when the change is 177 // made. 178 func (client *api) GenerateDomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) { 179 if client.debug { 180 debugRecords("GenDC input", existing) 181 } 182 183 var corrections = []*models.Correction{} 184 185 // diff existing vs. current. 186 differ := diff.New(dc) 187 keysToUpdate := differ.ChangedGroups(existing) 188 if client.debug { 189 diff.DebugKeyMapMap("GenDC diff", keysToUpdate) 190 } 191 if len(keysToUpdate) == 0 { 192 return nil, nil 193 } 194 195 // Regroup data by FQDN. ChangedGroups returns data grouped by label:RType tuples. 196 affectedLabels, msgsForLabel := gatherAffectedLabels(keysToUpdate) 197 _, desiredRecords := dc.Records.GroupedByFQDN() 198 doesLabelExist := existing.FQDNMap() 199 200 g := gandi.NewLiveDNSClient(client.apikey, gandi.Config{SharingID: client.sharingid, Debug: client.debug}) 201 202 // For any key with an update, delete or replace those records. 203 for label := range affectedLabels { 204 if len(desiredRecords[label]) == 0 { 205 // No records matching this key? This can only mean that all 206 // the records were deleted. Delete them. 207 208 msgs := strings.Join(msgsForLabel[label], "\n") 209 domain := dc.Name 210 shortname := dnsutil.TrimDomainName(label, dc.Name) 211 corrections = append(corrections, 212 &models.Correction{ 213 Msg: msgs, 214 F: func() error { 215 err := g.DeleteDomainRecords(domain, shortname) 216 if err != nil { 217 return err 218 } 219 return nil 220 }, 221 }) 222 223 } else { 224 // Replace all the records at a label with our new records. 225 226 // Generate the new data in Gandi's format. 227 ns := recordsToNative(desiredRecords[label], dc.Name) 228 229 if doesLabelExist[label] { 230 // Records exist for this label. Replace them with what we have. 231 232 msg := strings.Join(msgsForLabel[label], "\n") 233 domain := dc.Name 234 shortname := dnsutil.TrimDomainName(label, dc.Name) 235 corrections = append(corrections, 236 &models.Correction{ 237 Msg: msg, 238 F: func() error { 239 res, err := g.UpdateDomainRecordsByName(domain, shortname, ns) 240 if err != nil { 241 return fmt.Errorf("%+v: %w", res, err) 242 } 243 return nil 244 }, 245 }) 246 247 } else { 248 // First time putting data on this label. Create it. 249 250 // We have to create the label one rtype at a time. 251 for _, n := range ns { 252 msg := strings.Join(msgsForLabel[label], "\n") 253 domain := dc.Name 254 shortname := dnsutil.TrimDomainName(label, dc.Name) 255 rtype := n.RrsetType 256 ttl := n.RrsetTTL 257 values := n.RrsetValues 258 corrections = append(corrections, 259 &models.Correction{ 260 Msg: msg, 261 F: func() error { 262 res, err := g.CreateDomainRecord(domain, shortname, rtype, ttl, values) 263 if err != nil { 264 return fmt.Errorf("%+v: %w", res, err) 265 } 266 return nil 267 }, 268 }) 269 } 270 } 271 } 272 } 273 274 return corrections, nil 275 } 276 277 // debugRecords prints a list of RecordConfig. 278 func debugRecords(note string, recs []*models.RecordConfig) { 279 fmt.Println("DEBUG:", note) 280 for k, v := range recs { 281 fmt.Printf(" %v: %v %v %v %v\n", k, v.GetLabel(), v.Type, v.TTL, v.GetTargetCombined()) 282 } 283 } 284 285 // gatherAffectedLabels takes the output of diff.ChangedGroups and 286 // regroups it by FQDN of the label, not by Key. It also returns 287 // a list of all the FQDNs. 288 func gatherAffectedLabels(groups map[models.RecordKey][]string) (labels map[string]bool, msgs map[string][]string) { 289 labels = map[string]bool{} 290 msgs = map[string][]string{} 291 for k, v := range groups { 292 labels[k.NameFQDN] = true 293 msgs[k.NameFQDN] = append(msgs[k.NameFQDN], v...) 294 } 295 return labels, msgs 296 } 297 298 // Section 3: Registrar-related functions 299 300 // GetNameservers returns a list of nameservers for domain. 301 func (client *api) GetNameservers(domain string) ([]*models.Nameserver, error) { 302 g := gandi.NewLiveDNSClient(client.apikey, gandi.Config{SharingID: client.sharingid, Debug: client.debug}) 303 nameservers, err := g.GetDomainNS(domain) 304 if err != nil { 305 return nil, err 306 } 307 return models.StringsToNameservers(nameservers), nil 308 } 309 310 // GetRegistrarCorrections returns a list of corrections for this registrar. 311 func (client *api) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 312 gd := gandi.NewDomainClient(client.apikey, gandi.Config{SharingID: client.sharingid, Debug: client.debug}) 313 314 existingNs, err := gd.GetNameServers(dc.Name) 315 if err != nil { 316 return nil, err 317 } 318 sort.Strings(existingNs) 319 existing := strings.Join(existingNs, ",") 320 321 desiredNs := models.NameserversToStrings(dc.Nameservers) 322 sort.Strings(desiredNs) 323 desired := strings.Join(desiredNs, ",") 324 325 if existing != desired { 326 return []*models.Correction{ 327 { 328 Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", existing, desired), 329 F: func() (err error) { 330 err = gd.UpdateNameServers(dc.Name, desiredNs) 331 return 332 }}, 333 }, nil 334 } 335 return nil, nil 336 }