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