github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/gandi_v5/gandi_v5Provider.go (about)

     1  package gandi5
     2  
     3  /*
     4  
     5  Gandi API V5 LiveDNS provider:
     6  
     7  Documentation: https://api.gandi.net/docs/
     8  Endpoint: https://api.gandi.net/
     9  
    10  Settings from `creds.json`:
    11     - apikey
    12     - sharing_id (optional)
    13  
    14  */
    15  
    16  import (
    17  	"encoding/json"
    18  	"fmt"
    19  	"os"
    20  	"sort"
    21  	"strconv"
    22  	"strings"
    23  
    24  	"github.com/miekg/dns/dnsutil"
    25  	gandi "github.com/tiramiseb/go-gandi"
    26  
    27  	"github.com/StackExchange/dnscontrol/v2/models"
    28  	"github.com/StackExchange/dnscontrol/v2/pkg/printer"
    29  	"github.com/StackExchange/dnscontrol/v2/providers"
    30  	"github.com/StackExchange/dnscontrol/v2/providers/diff"
    31  )
    32  
    33  // Section 1: Register this provider in the system.
    34  
    35  // init registers the provider to dnscontrol.
    36  func init() {
    37  	providers.RegisterDomainServiceProviderType("GANDI_V5", newDsp, features)
    38  	providers.RegisterRegistrarType("GANDI_V5", newReg)
    39  }
    40  
    41  // features declares which features and options are available.
    42  var features = providers.DocumentationNotes{
    43  	providers.CanUseCAA:              providers.Can(),
    44  	providers.CanUsePTR:              providers.Can(),
    45  	providers.CanUseSRV:              providers.Can(),
    46  	providers.CantUseNOPURGE:         providers.Cannot(),
    47  	providers.DocCreateDomains:       providers.Cannot("Can only manage domains registered through their service"),
    48  	providers.DocOfficiallySupported: providers.Cannot(),
    49  	providers.CanGetZones:            providers.Can(),
    50  }
    51  
    52  // Section 2: Define the API client.
    53  
    54  // api is the api handle used to store any client-related state.
    55  type api struct {
    56  	apikey    string
    57  	sharingid string
    58  	debug     bool
    59  }
    60  
    61  // newDsp generates a DNS Service Provider client handle.
    62  func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
    63  	return newHelper(conf, metadata)
    64  }
    65  
    66  // newReg generates a Registrar Provider client handle.
    67  func newReg(conf map[string]string) (providers.Registrar, error) {
    68  	return newHelper(conf, nil)
    69  }
    70  
    71  // newHelper generates a handle.
    72  func newHelper(m map[string]string, metadata json.RawMessage) (*api, error) {
    73  	api := &api{}
    74  	api.apikey = m["apikey"]
    75  	if api.apikey == "" {
    76  		return nil, fmt.Errorf("missing Gandi apikey")
    77  	}
    78  	api.sharingid = m["sharing_id"]
    79  	debug, err := strconv.ParseBool(os.Getenv("GANDI_V5_DEBUG"))
    80  	if err == nil {
    81  		api.debug = debug
    82  	}
    83  
    84  	return api, nil
    85  }
    86  
    87  // Section 3: Domain Service Provider (DSP) related functions
    88  
    89  // NB(tal): To future-proof your code, all new providers should
    90  // implement GetDomainCorrections exactly as you see here
    91  // (byte-for-byte the same). In 3.0
    92  // we plan on using just the individual calls to GetZoneRecords,
    93  // PostProcessRecords, and so on.
    94  //
    95  // Currently every provider does things differently, which prevents
    96  // us from doing things like using GetZoneRecords() of a provider
    97  // to make convertzone work with all providers.
    98  
    99  // GetDomainCorrections get the current and existing records,
   100  // post-process them, and generate corrections.
   101  func (client *api) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
   102  	existing, err := client.GetZoneRecords(dc.Name)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	models.PostProcessRecords(existing)
   107  	clean := PrepFoundRecords(existing)
   108  	PrepDesiredRecords(dc)
   109  	return client.GenerateDomainCorrections(dc, clean)
   110  }
   111  
   112  // GetZoneRecords gathers the DNS records and converts them to
   113  // dnscontrol's format.
   114  func (client *api) GetZoneRecords(domain string) (models.Records, error) {
   115  	g := gandi.NewLiveDNSClient(client.apikey, gandi.Config{SharingID: client.sharingid, Debug: client.debug})
   116  
   117  	// Get all the existing records:
   118  	records, err := g.ListDomainRecords(domain)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	// Convert them to DNScontrol's native format:
   124  	existingRecords := []*models.RecordConfig{}
   125  	for _, rr := range records {
   126  		existingRecords = append(existingRecords, nativeToRecords(rr, domain)...)
   127  	}
   128  
   129  	return existingRecords, nil
   130  }
   131  
   132  // PrepFoundRecords munges any records to make them compatible with
   133  // this provider. Usually this is a no-op.
   134  func PrepFoundRecords(recs models.Records) models.Records {
   135  	// If there are records that need to be modified, removed, etc. we
   136  	// do it here.  Usually this is a no-op.
   137  	return recs
   138  }
   139  
   140  // PrepDesiredRecords munges any records to best suit this provider.
   141  func PrepDesiredRecords(dc *models.DomainConfig) {
   142  	// Sort through the dc.Records, eliminate any that can't be
   143  	// supported; modify any that need adjustments to work with the
   144  	// provider.  We try to do minimal changes otherwise it gets
   145  	// confusing.
   146  
   147  	dc.Punycode()
   148  
   149  	recordsToKeep := make([]*models.RecordConfig, 0, len(dc.Records))
   150  	for _, rec := range dc.Records {
   151  		if rec.TTL < 300 {
   152  			printer.Warnf("Gandi does not support ttls < 300. Setting %s from %d to 300\n", rec.GetLabelFQDN(), rec.TTL)
   153  			rec.TTL = 300
   154  		}
   155  		if rec.TTL > 2592000 {
   156  			printer.Warnf("Gandi does not support ttls > 30 days. Setting %s from %d to 2592000\n", rec.GetLabelFQDN(), rec.TTL)
   157  			rec.TTL = 2592000
   158  		}
   159  		if rec.Type == "TXT" {
   160  			rec.SetTarget("\"" + rec.GetTargetField() + "\"") // FIXME(tlim): Should do proper quoting.
   161  		}
   162  		if rec.Type == "NS" && rec.GetLabel() == "@" {
   163  			if !strings.HasSuffix(rec.GetTargetField(), ".gandi.net.") {
   164  				printer.Warnf("Gandi does not support changing apex NS records. Ignoring %s\n", rec.GetTargetField())
   165  			}
   166  			continue
   167  		}
   168  		recordsToKeep = append(recordsToKeep, rec)
   169  	}
   170  	dc.Records = recordsToKeep
   171  }
   172  
   173  // GenerateDomainCorrections takes the desired and existing records
   174  // and produces a Correction list.  The correction list is simply
   175  // a list of functions to call to actually make the desired
   176  // correction, and a message to output to the user when the change is
   177  // made.
   178  func (client *api) GenerateDomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
   179  	if client.debug {
   180  		debugRecords("GenDC input", existing)
   181  	}
   182  
   183  	var corrections = []*models.Correction{}
   184  
   185  	// diff existing vs. current.
   186  	differ := diff.New(dc)
   187  	keysToUpdate := differ.ChangedGroups(existing)
   188  	if client.debug {
   189  		diff.DebugKeyMapMap("GenDC diff", keysToUpdate)
   190  	}
   191  	if len(keysToUpdate) == 0 {
   192  		return nil, nil
   193  	}
   194  
   195  	// Regroup data by FQDN.  ChangedGroups returns data grouped by label:RType tuples.
   196  	affectedLabels, msgsForLabel := gatherAffectedLabels(keysToUpdate)
   197  	_, desiredRecords := dc.Records.GroupedByFQDN()
   198  	doesLabelExist := existing.FQDNMap()
   199  
   200  	g := gandi.NewLiveDNSClient(client.apikey, gandi.Config{SharingID: client.sharingid, Debug: client.debug})
   201  
   202  	// For any key with an update, delete or replace those records.
   203  	for label := range affectedLabels {
   204  		if len(desiredRecords[label]) == 0 {
   205  			// No records matching this key?  This can only mean that all
   206  			// the records were deleted. Delete them.
   207  
   208  			msgs := strings.Join(msgsForLabel[label], "\n")
   209  			domain := dc.Name
   210  			shortname := dnsutil.TrimDomainName(label, dc.Name)
   211  			corrections = append(corrections,
   212  				&models.Correction{
   213  					Msg: msgs,
   214  					F: func() error {
   215  						err := g.DeleteDomainRecords(domain, shortname)
   216  						if err != nil {
   217  							return err
   218  						}
   219  						return nil
   220  					},
   221  				})
   222  
   223  		} else {
   224  			// Replace all the records at a label with our new records.
   225  
   226  			// Generate the new data in Gandi's format.
   227  			ns := recordsToNative(desiredRecords[label], dc.Name)
   228  
   229  			if doesLabelExist[label] {
   230  				// Records exist for this label. Replace them with what we have.
   231  
   232  				msg := strings.Join(msgsForLabel[label], "\n")
   233  				domain := dc.Name
   234  				shortname := dnsutil.TrimDomainName(label, dc.Name)
   235  				corrections = append(corrections,
   236  					&models.Correction{
   237  						Msg: msg,
   238  						F: func() error {
   239  							res, err := g.UpdateDomainRecordsByName(domain, shortname, ns)
   240  							if err != nil {
   241  								return fmt.Errorf("%+v: %w", res, err)
   242  							}
   243  							return nil
   244  						},
   245  					})
   246  
   247  			} else {
   248  				// First time putting data on this label. Create it.
   249  
   250  				// We have to create the label one rtype at a time.
   251  				for _, n := range ns {
   252  					msg := strings.Join(msgsForLabel[label], "\n")
   253  					domain := dc.Name
   254  					shortname := dnsutil.TrimDomainName(label, dc.Name)
   255  					rtype := n.RrsetType
   256  					ttl := n.RrsetTTL
   257  					values := n.RrsetValues
   258  					corrections = append(corrections,
   259  						&models.Correction{
   260  							Msg: msg,
   261  							F: func() error {
   262  								res, err := g.CreateDomainRecord(domain, shortname, rtype, ttl, values)
   263  								if err != nil {
   264  									return fmt.Errorf("%+v: %w", res, err)
   265  								}
   266  								return nil
   267  							},
   268  						})
   269  				}
   270  			}
   271  		}
   272  	}
   273  
   274  	return corrections, nil
   275  }
   276  
   277  // debugRecords prints a list of RecordConfig.
   278  func debugRecords(note string, recs []*models.RecordConfig) {
   279  	fmt.Println("DEBUG:", note)
   280  	for k, v := range recs {
   281  		fmt.Printf("   %v: %v %v %v %v\n", k, v.GetLabel(), v.Type, v.TTL, v.GetTargetCombined())
   282  	}
   283  }
   284  
   285  // gatherAffectedLabels takes the output of diff.ChangedGroups and
   286  // regroups it by FQDN of the label, not by Key. It also returns
   287  // a list of all the FQDNs.
   288  func gatherAffectedLabels(groups map[models.RecordKey][]string) (labels map[string]bool, msgs map[string][]string) {
   289  	labels = map[string]bool{}
   290  	msgs = map[string][]string{}
   291  	for k, v := range groups {
   292  		labels[k.NameFQDN] = true
   293  		msgs[k.NameFQDN] = append(msgs[k.NameFQDN], v...)
   294  	}
   295  	return labels, msgs
   296  }
   297  
   298  // Section 3: Registrar-related functions
   299  
   300  // GetNameservers returns a list of nameservers for domain.
   301  func (client *api) GetNameservers(domain string) ([]*models.Nameserver, error) {
   302  	g := gandi.NewLiveDNSClient(client.apikey, gandi.Config{SharingID: client.sharingid, Debug: client.debug})
   303  	nameservers, err := g.GetDomainNS(domain)
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  	return models.StringsToNameservers(nameservers), nil
   308  }
   309  
   310  // GetRegistrarCorrections returns a list of corrections for this registrar.
   311  func (client *api) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
   312  	gd := gandi.NewDomainClient(client.apikey, gandi.Config{SharingID: client.sharingid, Debug: client.debug})
   313  
   314  	existingNs, err := gd.GetNameServers(dc.Name)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  	sort.Strings(existingNs)
   319  	existing := strings.Join(existingNs, ",")
   320  
   321  	desiredNs := models.NameserversToStrings(dc.Nameservers)
   322  	sort.Strings(desiredNs)
   323  	desired := strings.Join(desiredNs, ",")
   324  
   325  	if existing != desired {
   326  		return []*models.Correction{
   327  			{
   328  				Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", existing, desired),
   329  				F: func() (err error) {
   330  					err = gd.UpdateNameServers(dc.Name, desiredNs)
   331  					return
   332  				}},
   333  		}, nil
   334  	}
   335  	return nil, nil
   336  }