github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/dnsimple/dnsimpleProvider.go (about)

     1  package dnsimple
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"sort"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/StackExchange/dnscontrol/models"
    11  	"github.com/StackExchange/dnscontrol/providers"
    12  	"github.com/StackExchange/dnscontrol/providers/diff"
    13  	"github.com/miekg/dns/dnsutil"
    14  
    15  	dnsimpleapi "github.com/dnsimple/dnsimple-go/dnsimple"
    16  )
    17  
    18  var docNotes = providers.DocumentationNotes{
    19  	providers.DocDualHost:            providers.Cannot("DNSimple does not allow sufficient control over the apex NS records"),
    20  	providers.DocCreateDomains:       providers.Cannot(),
    21  	providers.DocOfficiallySupported: providers.Cannot(),
    22  	providers.CanUseTLSA:             providers.Cannot(),
    23  }
    24  
    25  func init() {
    26  	providers.RegisterRegistrarType("DNSIMPLE", newReg)
    27  	providers.RegisterDomainServiceProviderType("DNSIMPLE", newDsp, providers.CanUsePTR, providers.CanUseAlias, providers.CanUseCAA, providers.CanUseSRV, docNotes)
    28  }
    29  
    30  const stateRegistered = "registered"
    31  
    32  var defaultNameServerNames = []string{
    33  	"ns1.dnsimple.com",
    34  	"ns2.dnsimple.com",
    35  	"ns3.dnsimple.com",
    36  	"ns4.dnsimple.com",
    37  }
    38  
    39  type DnsimpleApi struct {
    40  	AccountToken string // The account access token
    41  	BaseURL      string // An alternate base URI
    42  	accountId    string // Account id cache
    43  }
    44  
    45  func (c *DnsimpleApi) GetNameservers(domainName string) ([]*models.Nameserver, error) {
    46  	return models.StringsToNameservers(defaultNameServerNames), nil
    47  }
    48  
    49  func (c *DnsimpleApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
    50  	corrections := []*models.Correction{}
    51  	dc.Punycode()
    52  	records, err := c.getRecords(dc.Name)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	var actual []*models.RecordConfig
    58  	for _, r := range records {
    59  		if r.Type == "SOA" || r.Type == "NS" {
    60  			continue
    61  		}
    62  		if r.Name == "" {
    63  			r.Name = "@"
    64  		}
    65  		if r.Type == "CNAME" || r.Type == "MX" || r.Type == "ALIAS" || r.Type == "SRV" {
    66  			r.Content += "."
    67  		}
    68  		// dnsimple adds these odd txt records that mirror the alias records.
    69  		// they seem to manage them on deletes and things, so we'll just pretend they don't exist
    70  		if r.Type == "TXT" && strings.HasPrefix(r.Content, "ALIAS for ") {
    71  			continue
    72  		}
    73  		rec := &models.RecordConfig{
    74  			NameFQDN:     dnsutil.AddOrigin(r.Name, dc.Name),
    75  			Type:         r.Type,
    76  			Target:       r.Content,
    77  			TTL:          uint32(r.TTL),
    78  			MxPreference: uint16(r.Priority),
    79  			Original:     r,
    80  		}
    81  		if r.Type == "CAA" || r.Type == "SRV" {
    82  			rec.CombinedTarget = true
    83  		}
    84  		actual = append(actual, rec)
    85  	}
    86  	removeOtherNS(dc)
    87  	dc.Filter(func(r *models.RecordConfig) bool {
    88  		if r.Type == "CAA" || r.Type == "SRV" {
    89  			r.MergeToTarget()
    90  		}
    91  		return true
    92  	})
    93  	differ := diff.New(dc)
    94  	_, create, delete, modify := differ.IncrementalDiff(actual)
    95  
    96  	for _, del := range delete {
    97  		rec := del.Existing.Original.(dnsimpleapi.ZoneRecord)
    98  		corrections = append(corrections, &models.Correction{
    99  			Msg: del.String(),
   100  			F:   c.deleteRecordFunc(rec.ID, dc.Name),
   101  		})
   102  	}
   103  
   104  	for _, cre := range create {
   105  		rec := cre.Desired
   106  		corrections = append(corrections, &models.Correction{
   107  			Msg: cre.String(),
   108  			F:   c.createRecordFunc(rec, dc.Name),
   109  		})
   110  	}
   111  
   112  	for _, mod := range modify {
   113  		old := mod.Existing.Original.(dnsimpleapi.ZoneRecord)
   114  		new := mod.Desired
   115  		corrections = append(corrections, &models.Correction{
   116  			Msg: mod.String(),
   117  			F:   c.updateRecordFunc(&old, new, dc.Name),
   118  		})
   119  	}
   120  
   121  	return corrections, nil
   122  }
   123  
   124  func (c *DnsimpleApi) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
   125  	corrections := []*models.Correction{}
   126  
   127  	nameServers, err := c.getNameservers(dc.Name)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	actual := strings.Join(nameServers, ",")
   133  
   134  	expectedSet := []string{}
   135  	for _, ns := range dc.Nameservers {
   136  		expectedSet = append(expectedSet, ns.Name)
   137  	}
   138  	sort.Strings(expectedSet)
   139  	expected := strings.Join(expectedSet, ",")
   140  
   141  	if actual != expected {
   142  		return []*models.Correction{
   143  			{
   144  				Msg: fmt.Sprintf("Update nameservers %s -> %s", actual, expected),
   145  				F:   c.updateNameserversFunc(expectedSet, dc.Name),
   146  			},
   147  		}, nil
   148  	}
   149  
   150  	return corrections, nil
   151  }
   152  
   153  // DNSimple calls
   154  
   155  func (c *DnsimpleApi) getClient() *dnsimpleapi.Client {
   156  	client := dnsimpleapi.NewClient(dnsimpleapi.NewOauthTokenCredentials(c.AccountToken))
   157  	if c.BaseURL != "" {
   158  		client.BaseURL = c.BaseURL
   159  	}
   160  	return client
   161  }
   162  
   163  func (c *DnsimpleApi) getAccountId() (string, error) {
   164  	if c.accountId == "" {
   165  		client := c.getClient()
   166  		whoamiResponse, err := client.Identity.Whoami()
   167  		if err != nil {
   168  			return "", err
   169  		}
   170  		if whoamiResponse.Data.User != nil && whoamiResponse.Data.Account == nil {
   171  			return "", fmt.Errorf("DNSimple token appears to be a user token. Please supply an account token")
   172  		}
   173  		c.accountId = strconv.Itoa(whoamiResponse.Data.Account.ID)
   174  	}
   175  	return c.accountId, nil
   176  }
   177  
   178  func (c *DnsimpleApi) getRecords(domainName string) ([]dnsimpleapi.ZoneRecord, error) {
   179  	client := c.getClient()
   180  
   181  	accountId, err := c.getAccountId()
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	opts := &dnsimpleapi.ZoneRecordListOptions{}
   187  	recs := []dnsimpleapi.ZoneRecord{}
   188  	opts.Page = 1
   189  	for {
   190  		recordsResponse, err := client.Zones.ListRecords(accountId, domainName, opts)
   191  		if err != nil {
   192  			return nil, err
   193  		}
   194  		recs = append(recs, recordsResponse.Data...)
   195  		pg := recordsResponse.Pagination
   196  		if pg.CurrentPage == pg.TotalPages {
   197  			break
   198  		}
   199  		opts.Page++
   200  	}
   201  
   202  	return recs, nil
   203  }
   204  
   205  // Returns the name server names that should be used. If the domain is registered
   206  // then this method will return the delegation name servers. If this domain
   207  // is hosted only, then it will return the default DNSimple name servers.
   208  func (c *DnsimpleApi) getNameservers(domainName string) ([]string, error) {
   209  	client := c.getClient()
   210  
   211  	accountId, err := c.getAccountId()
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  
   216  	domainResponse, err := client.Domains.GetDomain(accountId, domainName)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	if domainResponse.Data.State == stateRegistered {
   222  
   223  		delegationResponse, err := client.Registrar.GetDomainDelegation(accountId, domainName)
   224  		if err != nil {
   225  			return nil, err
   226  		}
   227  
   228  		return *delegationResponse.Data, nil
   229  	} else {
   230  		return defaultNameServerNames, nil
   231  	}
   232  }
   233  
   234  // Returns a function that can be invoked to change the delegation of the domain to the given name server names.
   235  func (c *DnsimpleApi) updateNameserversFunc(nameServerNames []string, domainName string) func() error {
   236  	return func() error {
   237  		client := c.getClient()
   238  
   239  		accountId, err := c.getAccountId()
   240  		if err != nil {
   241  			return err
   242  		}
   243  
   244  		nameServers := dnsimpleapi.Delegation(nameServerNames)
   245  
   246  		_, err = client.Registrar.ChangeDomainDelegation(accountId, domainName, &nameServers)
   247  		if err != nil {
   248  			return err
   249  		}
   250  
   251  		return nil
   252  	}
   253  }
   254  
   255  // Returns a function that can be invoked to create a record in a zone.
   256  func (c *DnsimpleApi) createRecordFunc(rc *models.RecordConfig, domainName string) func() error {
   257  	return func() error {
   258  		client := c.getClient()
   259  
   260  		accountId, err := c.getAccountId()
   261  		if err != nil {
   262  			return err
   263  		}
   264  		record := dnsimpleapi.ZoneRecord{
   265  			Name:     dnsutil.TrimDomainName(rc.NameFQDN, domainName),
   266  			Type:     rc.Type,
   267  			Content:  rc.Target,
   268  			TTL:      int(rc.TTL),
   269  			Priority: int(rc.MxPreference),
   270  		}
   271  		_, err = client.Zones.CreateRecord(accountId, domainName, record)
   272  		if err != nil {
   273  			return err
   274  		}
   275  
   276  		return nil
   277  	}
   278  }
   279  
   280  // Returns a function that can be invoked to delete a record in a zone.
   281  func (c *DnsimpleApi) deleteRecordFunc(recordId int, domainName string) func() error {
   282  	return func() error {
   283  		client := c.getClient()
   284  
   285  		accountId, err := c.getAccountId()
   286  		if err != nil {
   287  			return err
   288  		}
   289  
   290  		_, err = client.Zones.DeleteRecord(accountId, domainName, recordId)
   291  		if err != nil {
   292  			return err
   293  		}
   294  
   295  		return nil
   296  
   297  	}
   298  }
   299  
   300  // Returns a function that can be invoked to update a record in a zone.
   301  func (c *DnsimpleApi) updateRecordFunc(old *dnsimpleapi.ZoneRecord, rc *models.RecordConfig, domainName string) func() error {
   302  	return func() error {
   303  		client := c.getClient()
   304  
   305  		accountId, err := c.getAccountId()
   306  		if err != nil {
   307  			return err
   308  		}
   309  
   310  		record := dnsimpleapi.ZoneRecord{
   311  			Name:     dnsutil.TrimDomainName(rc.NameFQDN, domainName),
   312  			Type:     rc.Type,
   313  			Content:  rc.Target,
   314  			TTL:      int(rc.TTL),
   315  			Priority: int(rc.MxPreference),
   316  		}
   317  
   318  		_, err = client.Zones.UpdateRecord(accountId, domainName, old.ID, record)
   319  		if err != nil {
   320  			return err
   321  		}
   322  
   323  		return nil
   324  	}
   325  }
   326  
   327  // constructors
   328  
   329  func newReg(conf map[string]string) (providers.Registrar, error) {
   330  	return newProvider(conf, nil)
   331  }
   332  
   333  func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
   334  	return newProvider(conf, metadata)
   335  }
   336  
   337  func newProvider(m map[string]string, metadata json.RawMessage) (*DnsimpleApi, error) {
   338  	api := &DnsimpleApi{}
   339  	api.AccountToken = m["token"]
   340  	if api.AccountToken == "" {
   341  		return nil, fmt.Errorf("DNSimple token must be provided.")
   342  	}
   343  
   344  	if m["baseurl"] != "" {
   345  		api.BaseURL = m["baseurl"]
   346  	}
   347  
   348  	return api, nil
   349  }
   350  
   351  // remove all non-dnsimple NS records from our desired state.
   352  // if any are found, print a warning
   353  func removeOtherNS(dc *models.DomainConfig) {
   354  	newList := make([]*models.RecordConfig, 0, len(dc.Records))
   355  	for _, rec := range dc.Records {
   356  		if rec.Type == "NS" {
   357  			// apex NS inside dnsimple are expected.
   358  			if rec.NameFQDN == dc.Name && strings.HasSuffix(rec.Target, ".dnsimple.com.") {
   359  				continue
   360  			}
   361  			fmt.Printf("Warning: dnsimple.com does not allow NS records to be modified. %s will not be added.\n", rec.Target)
   362  			continue
   363  		}
   364  		newList = append(newList, rec)
   365  	}
   366  	dc.Records = newList
   367  }