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  }