github.com/philhug/dnscontrol@v0.2.4-0.20180625181521-921fa9849001/providers/gandi/livedns.go (about)

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