github.com/StackExchange/DNSControl@v0.2.8/providers/gandi/livedns.go (about) 1 package gandi 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/StackExchange/dnscontrol/models" 10 "github.com/StackExchange/dnscontrol/pkg/printer" 11 "github.com/StackExchange/dnscontrol/providers" 12 "github.com/StackExchange/dnscontrol/providers/diff" 13 "github.com/google/uuid" 14 "github.com/pkg/errors" 15 gandiclient "github.com/prasmussen/gandi-api/client" 16 gandilivedomain "github.com/prasmussen/gandi-api/live_dns/domain" 17 gandiliverecord "github.com/prasmussen/gandi-api/live_dns/record" 18 gandilivezone "github.com/prasmussen/gandi-api/live_dns/zone" 19 ) 20 21 var liveFeatures = providers.DocumentationNotes{ 22 providers.CanUseCAA: providers.Can(), 23 providers.CanUsePTR: providers.Can(), 24 providers.CanUseSRV: providers.Can(), 25 providers.CantUseNOPURGE: providers.Cannot(), 26 providers.DocCreateDomains: providers.Cannot("Can only manage domains registered through their service"), 27 providers.DocOfficiallySupported: providers.Cannot(), 28 } 29 30 func init() { 31 providers.RegisterDomainServiceProviderType("GANDI-LIVEDNS", newLiveDsp, liveFeatures) 32 } 33 34 func newLiveDsp(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 35 APIKey := m["apikey"] 36 if APIKey == "" { 37 return nil, errors.Errorf("missing Gandi apikey") 38 } 39 40 return newLiveClient(APIKey), nil 41 } 42 43 type domainManager interface { 44 Info(string) (*gandilivedomain.Info, error) 45 Records(string) gandiliverecord.Manager 46 } 47 48 type zoneManager interface { 49 InfoByUUID(uuid.UUID) (*gandilivezone.Info, error) 50 Create(gandilivezone.Info) (*gandilivezone.CreateStatus, error) 51 Set(string, gandilivezone.Info) (*gandilivezone.Status, error) 52 Records(gandilivezone.Info) gandiliverecord.Manager 53 } 54 55 type liveClient struct { 56 client *gandiclient.Client 57 zoneManager zoneManager 58 domainManager domainManager 59 } 60 61 func newLiveClient(APIKey string) *liveClient { 62 cl := gandiclient.New(APIKey, gandiclient.LiveDNS) 63 return &liveClient{ 64 client: cl, 65 zoneManager: gandilivezone.New(cl), 66 domainManager: gandilivedomain.New(cl), 67 } 68 } 69 70 // GetNameservers returns the list of gandi name servers for a given domain 71 func (c *liveClient) GetNameservers(domain string) ([]*models.Nameserver, error) { 72 domains := []string{} 73 response, err := c.client.Get("/nameservers/"+domain, &domains) 74 if err != nil { 75 return nil, errors.Errorf("failed to get nameservers for domain %s", domain) 76 } 77 defer response.Body.Close() 78 79 ns := []*models.Nameserver{} 80 for _, domain := range domains { 81 ns = append(ns, &models.Nameserver{Name: domain}) 82 } 83 return ns, nil 84 } 85 86 // GetDomainCorrections returns a list of corrections recommended for this domain. 87 func (c *liveClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 88 dc.Punycode() 89 records, err := c.domainManager.Records(dc.Name).List() 90 if err != nil { 91 return nil, err 92 } 93 foundRecords := c.recordConfigFromInfo(records, dc.Name) 94 recordsToKeep, records, err := c.recordsToInfo(dc.Records) 95 if err != nil { 96 return nil, err 97 } 98 dc.Records = recordsToKeep 99 100 // Normalize 101 models.PostProcessRecords(foundRecords) 102 103 differ := diff.New(dc) 104 105 _, create, del, mod := differ.IncrementalDiff(foundRecords) 106 if len(create)+len(del)+len(mod) > 0 { 107 message := fmt.Sprintf("Setting dns records for %s:", dc.Name) 108 for _, record := range dc.Records { 109 message += "\n" + record.GetTargetCombined() 110 } 111 return []*models.Correction{ 112 { 113 Msg: message, 114 F: func() error { 115 return c.createZone(dc.Name, records) 116 }, 117 }, 118 }, nil 119 } 120 return []*models.Correction{}, nil 121 } 122 123 // createZone creates a new empty zone for the domain, populates it with the record infos and associates the domain to it 124 func (c *liveClient) createZone(domainname string, records []*gandiliverecord.Info) error { 125 domainInfo, err := c.domainManager.Info(domainname) 126 infos, err := c.zoneManager.InfoByUUID(*domainInfo.ZoneUUID) 127 if err != nil { 128 return err 129 } 130 infos.Name = fmt.Sprintf("zone created by dnscontrol for %s on %s", domainname, time.Now().Format(time.RFC3339)) 131 printer.Debugf("DEBUG: createZone SharingID=%v\n", infos.SharingID) 132 133 // duplicate zone Infos 134 status, err := c.zoneManager.Create(*infos) 135 if err != nil { 136 return err 137 } 138 zoneInfos, err := c.zoneManager.InfoByUUID(*status.UUID) 139 if err != nil { 140 // gandi might take some time to make the new zone available 141 for i := 0; i < 10; i++ { 142 printer.Printf("zone info not yet available. Delay and retry: %s\n", err.Error()) 143 time.Sleep(100 * time.Millisecond) 144 zoneInfos, err = c.zoneManager.InfoByUUID(*status.UUID) 145 if err == nil { 146 break 147 } 148 } 149 } 150 if err != nil { 151 return err 152 } 153 recordManager := c.zoneManager.Records(*zoneInfos) 154 for _, record := range records { 155 _, err := recordManager.Create(*record) 156 if err != nil { 157 return err 158 } 159 } 160 _, err = c.zoneManager.Set(domainname, *zoneInfos) 161 if err != nil { 162 return err 163 } 164 165 return nil 166 } 167 168 // recordConfigFromInfo takes a DNS record from Gandi liveDNS and returns our native RecordConfig format. 169 func (c *liveClient) recordConfigFromInfo(infos []*gandiliverecord.Info, origin string) []*models.RecordConfig { 170 rcs := []*models.RecordConfig{} 171 for _, info := range infos { 172 // TXT records might have multiple values. In that case, 173 // they are all for the TXT record at that label. 174 if info.Type == "TXT" { 175 rc := &models.RecordConfig{ 176 Type: info.Type, 177 Original: info, 178 TTL: uint32(info.TTL), 179 } 180 rc.SetLabel(info.Name, origin) 181 var parsed []string 182 for _, txt := range info.Values { 183 parsed = append(parsed, models.StripQuotes(txt)) 184 } 185 err := rc.SetTargetTXTs(parsed) 186 if err != nil { 187 panic(errors.Wrapf(err, "recordConfigFromInfo=TXT failed")) 188 } 189 rcs = append(rcs, rc) 190 } else { 191 // All other record types might have multiple values, but that means 192 // we should create one Recordconfig for each one. 193 for _, value := range info.Values { 194 rc := &models.RecordConfig{ 195 Type: info.Type, 196 Original: info, 197 TTL: uint32(info.TTL), 198 } 199 rc.SetLabel(info.Name, origin) 200 switch rtype := info.Type; rtype { 201 default: 202 err := rc.PopulateFromString(rtype, value, origin) 203 if err != nil { 204 panic(errors.Wrapf(err, "recordConfigFromInfo failed")) 205 } 206 } 207 rcs = append(rcs, rc) 208 } 209 } 210 } 211 return rcs 212 } 213 214 // recordsToInfo generates gandi record sets and filters incompatible entries from native records format 215 func (c *liveClient) recordsToInfo(records models.Records) (models.Records, []*gandiliverecord.Info, error) { 216 recordSets := map[string]map[string]*gandiliverecord.Info{} 217 recordInfos := []*gandiliverecord.Info{} 218 recordToKeep := models.Records{} 219 220 for _, rec := range records { 221 if rec.TTL < 300 { 222 printer.Warnf("Gandi does not support ttls < 300. %s will not be set to %d.\n", rec.GetLabelFQDN(), rec.TTL) 223 rec.TTL = 300 224 } 225 if rec.TTL > 2592000 { 226 return nil, nil, errors.Errorf("ERROR: Gandi does not support TTLs > 30 days (TTL=%d)", rec.TTL) 227 } 228 if rec.Type == "NS" && rec.GetLabel() == "@" { 229 if !strings.HasSuffix(rec.GetTargetField(), ".gandi.net.") { 230 printer.Warnf("Gandi does not support changing apex NS records. %s will not be added.\n", rec.GetTargetField()) 231 } 232 continue 233 } 234 r, ok := recordSets[rec.GetLabel()][rec.Type] 235 if !ok { 236 _, ok := recordSets[rec.GetLabel()] 237 if !ok { 238 recordSets[rec.GetLabel()] = map[string]*gandiliverecord.Info{} 239 } 240 r = &gandiliverecord.Info{ 241 Type: rec.Type, 242 Name: rec.GetLabel(), 243 TTL: int64(rec.TTL), 244 } 245 recordInfos = append(recordInfos, r) 246 recordSets[rec.GetLabel()][rec.Type] = r 247 } else { 248 if r.TTL != int64(rec.TTL) { 249 printer.Warnf( 250 "Gandi liveDNS API does not support different TTL for the couple fqdn/type. Will use TTL of %d for %s %s\n", 251 r.TTL, 252 r.Type, 253 r.Name, 254 ) 255 } 256 } 257 recordToKeep = append(recordToKeep, rec) 258 if rec.Type == "TXT" { 259 for _, t := range rec.TxtStrings { 260 r.Values = append(r.Values, "\""+t+"\"") // FIXME(tlim): Should do proper quoting. 261 } 262 } else { 263 r.Values = append(r.Values, rec.GetTargetCombined()) 264 } 265 } 266 return recordToKeep, recordInfos, nil 267 }