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  }