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

     1  package linode
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  
     9  	"github.com/StackExchange/dnscontrol/models"
    10  	"github.com/StackExchange/dnscontrol/providers"
    11  	"github.com/StackExchange/dnscontrol/providers/diff"
    12  	"github.com/miekg/dns/dnsutil"
    13  	"github.com/pkg/errors"
    14  
    15  	"net/url"
    16  
    17  	"regexp"
    18  	"strings"
    19  
    20  	"golang.org/x/oauth2"
    21  )
    22  
    23  /*
    24  
    25  Linode API DNS provider:
    26  
    27  Info required in `creds.json`:
    28     - token
    29  
    30  */
    31  
    32  var allowedTTLValues = []uint32{
    33  	300,     // 5 minutes
    34  	3600,    // 1 hour
    35  	7200,    // 2 hours
    36  	14400,   // 4 hours
    37  	28800,   // 8 hours
    38  	57600,   // 16 hours
    39  	86400,   // 1 day
    40  	172800,  // 2 days
    41  	345600,  // 4 days
    42  	604800,  // 1 week
    43  	1209600, // 2 weeks
    44  	2419200, // 4 weeks
    45  }
    46  
    47  var srvRegexp = regexp.MustCompile(`^_(?P<Service>\w+)\.\_(?P<Protocol>\w+)$`)
    48  
    49  // LinodeApi is the handle for this provider.
    50  type LinodeApi struct {
    51  	client      *http.Client
    52  	baseURL     *url.URL
    53  	domainIndex map[string]int
    54  }
    55  
    56  var defaultNameServerNames = []string{
    57  	"ns1.linode.com",
    58  	"ns2.linode.com",
    59  	"ns3.linode.com",
    60  	"ns4.linode.com",
    61  	"ns5.linode.com",
    62  }
    63  
    64  // NewLinode creates the provider.
    65  func NewLinode(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
    66  	if m["token"] == "" {
    67  		return nil, errors.Errorf("Missing Linode token")
    68  	}
    69  
    70  	ctx := context.Background()
    71  	client := oauth2.NewClient(
    72  		ctx,
    73  		oauth2.StaticTokenSource(&oauth2.Token{AccessToken: m["token"]}),
    74  	)
    75  
    76  	baseURL, err := url.Parse(defaultBaseURL)
    77  	if err != nil {
    78  		return nil, errors.Errorf("Linode base URL not valid")
    79  	}
    80  
    81  	api := &LinodeApi{client: client, baseURL: baseURL}
    82  
    83  	// Get a domain to validate the token
    84  	if err := api.fetchDomainList(); err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	return api, nil
    89  }
    90  
    91  var features = providers.DocumentationNotes{
    92  	providers.DocDualHost:            providers.Cannot(),
    93  	providers.DocOfficiallySupported: providers.Cannot(),
    94  }
    95  
    96  func init() {
    97  	// SRV support is in this provider, but Linode doesn't seem to support it properly
    98  	providers.RegisterDomainServiceProviderType("LINODE", NewLinode, features)
    99  }
   100  
   101  // GetNameservers returns the nameservers for a domain.
   102  func (api *LinodeApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
   103  	return models.StringsToNameservers(defaultNameServerNames), nil
   104  }
   105  
   106  // GetDomainCorrections returns the corrections for a domain.
   107  func (api *LinodeApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
   108  	dc, err := dc.Copy()
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	dc.Punycode()
   114  
   115  	if api.domainIndex == nil {
   116  		if err := api.fetchDomainList(); err != nil {
   117  			return nil, err
   118  		}
   119  	}
   120  	domainID, ok := api.domainIndex[dc.Name]
   121  	if !ok {
   122  		return nil, errors.Errorf("%s not listed in domains for Linode account", dc.Name)
   123  	}
   124  
   125  	records, err := api.getRecords(domainID)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	existingRecords := make([]*models.RecordConfig, len(records), len(records)+len(defaultNameServerNames))
   131  	for i := range records {
   132  		existingRecords[i] = toRc(dc, &records[i])
   133  	}
   134  
   135  	// Linode always has read-only NS servers, but these are not mentioned in the API response
   136  	// https://github.com/linode/manager/blob/edd99dc4e1be5ab8190f243c3dbf8b830716255e/src/constants.js#L184
   137  	for _, name := range defaultNameServerNames {
   138  		rc := &models.RecordConfig{
   139  			Type:     "NS",
   140  			Original: &domainRecord{},
   141  		}
   142  		rc.SetLabelFromFQDN(dc.Name, dc.Name)
   143  		rc.SetTarget(name)
   144  
   145  		existingRecords = append(existingRecords, rc)
   146  	}
   147  
   148  	// Normalize
   149  	models.PostProcessRecords(existingRecords)
   150  
   151  	// Linode doesn't allow selecting an arbitrary TTL, only a set of predefined values
   152  	// We need to make sure we don't change it every time if it is as close as it's going to get
   153  	// By experimentation, Linode always rounds up. 300 -> 300, 301 -> 3600.
   154  	// https://github.com/linode/manager/blob/edd99dc4e1be5ab8190f243c3dbf8b830716255e/src/domains/components/SelectDNSSeconds.js#L19
   155  	for _, record := range dc.Records {
   156  		record.TTL = fixTTL(record.TTL)
   157  	}
   158  
   159  	differ := diff.New(dc)
   160  	_, create, del, modify := differ.IncrementalDiff(existingRecords)
   161  
   162  	var corrections []*models.Correction
   163  
   164  	// Deletes first so changing type works etc.
   165  	for _, m := range del {
   166  		id := m.Existing.Original.(*domainRecord).ID
   167  		if id == 0 { // Skip ID 0, these are the default nameservers always present
   168  			continue
   169  		}
   170  		corr := &models.Correction{
   171  			Msg: fmt.Sprintf("%s, Linode ID: %d", m.String(), id),
   172  			F: func() error {
   173  				return api.deleteRecord(domainID, id)
   174  			},
   175  		}
   176  		corrections = append(corrections, corr)
   177  	}
   178  	for _, m := range create {
   179  		req, err := toReq(dc, m.Desired)
   180  		if err != nil {
   181  			return nil, err
   182  		}
   183  		j, err := json.Marshal(req)
   184  		if err != nil {
   185  			return nil, err
   186  		}
   187  		corr := &models.Correction{
   188  			Msg: fmt.Sprintf("%s: %s", m.String(), string(j)),
   189  			F: func() error {
   190  				record, err := api.createRecord(domainID, req)
   191  				if err != nil {
   192  					return err
   193  				}
   194  				// TTL isn't saved when creating a record, so we will need to modify it immediately afterwards
   195  				return api.modifyRecord(domainID, record.ID, req)
   196  			},
   197  		}
   198  		corrections = append(corrections, corr)
   199  	}
   200  	for _, m := range modify {
   201  		id := m.Existing.Original.(*domainRecord).ID
   202  		if id == 0 { // Skip ID 0, these are the default nameservers always present
   203  			continue
   204  		}
   205  		req, err := toReq(dc, m.Desired)
   206  		if err != nil {
   207  			return nil, err
   208  		}
   209  		j, err := json.Marshal(req)
   210  		if err != nil {
   211  			return nil, err
   212  		}
   213  		corr := &models.Correction{
   214  			Msg: fmt.Sprintf("%s, Linode ID: %d: %s", m.String(), id, string(j)),
   215  			F: func() error {
   216  				return api.modifyRecord(domainID, id, req)
   217  			},
   218  		}
   219  		corrections = append(corrections, corr)
   220  	}
   221  
   222  	return corrections, nil
   223  }
   224  
   225  func toRc(dc *models.DomainConfig, r *domainRecord) *models.RecordConfig {
   226  	rc := &models.RecordConfig{
   227  		Type:         r.Type,
   228  		TTL:          r.TTLSec,
   229  		MxPreference: r.Priority,
   230  		SrvPriority:  r.Priority,
   231  		SrvWeight:    r.Weight,
   232  		SrvPort:      uint16(r.Port),
   233  		Original:     r,
   234  	}
   235  	rc.SetLabel(r.Name, dc.Name)
   236  
   237  	switch rtype := r.Type; rtype { // #rtype_variations
   238  	case "TXT":
   239  		rc.SetTargetTXT(r.Target)
   240  	case "CNAME", "MX", "NS", "SRV":
   241  		rc.SetTarget(dnsutil.AddOrigin(r.Target+".", dc.Name))
   242  	default:
   243  		rc.SetTarget(r.Target)
   244  	}
   245  
   246  	return rc
   247  }
   248  
   249  func toReq(dc *models.DomainConfig, rc *models.RecordConfig) (*recordEditRequest, error) {
   250  	req := &recordEditRequest{
   251  		Type:     rc.Type,
   252  		Name:     rc.GetLabel(),
   253  		Target:   rc.GetTargetField(),
   254  		TTL:      int(rc.TTL),
   255  		Priority: 0,
   256  		Port:     int(rc.SrvPort),
   257  		Weight:   int(rc.SrvWeight),
   258  	}
   259  
   260  	// Linode doesn't use "@", it uses an empty name
   261  	if req.Name == "@" {
   262  		req.Name = ""
   263  	}
   264  
   265  	// Linode uses the same property for MX and SRV priority
   266  	switch rc.Type { // #rtype_variations
   267  	case "A", "AAAA", "NS", "PTR", "TXT", "SOA", "TLSA", "CAA":
   268  		// Nothing special.
   269  	case "MX":
   270  		req.Priority = int(rc.MxPreference)
   271  		req.Target = fixTarget(req.Target, dc.Name)
   272  	case "SRV":
   273  		req.Priority = int(rc.SrvPriority)
   274  
   275  		// From softlayer provider
   276  		// This is to support SRV, it doesn't work yet for Linode
   277  		result := srvRegexp.FindStringSubmatch(req.Name)
   278  
   279  		if len(result) != 3 {
   280  			return nil, errors.Errorf("SRV Record must match format \"_service._protocol\" not %s", req.Name)
   281  		}
   282  
   283  		var serviceName, protocol string = result[1], strings.ToLower(result[2])
   284  
   285  		req.Protocol = protocol
   286  		req.Service = serviceName
   287  		req.Name = ""
   288  	case "CNAME":
   289  		req.Target = fixTarget(req.Target, dc.Name)
   290  	default:
   291  		msg := fmt.Sprintf("linode.toReq rtype %v unimplemented", rc.Type)
   292  		panic(msg)
   293  		// We panic so that we quickly find any switch statements
   294  		// that have not been updated for a new RR type.
   295  	}
   296  
   297  	return req, nil
   298  }
   299  
   300  func fixTarget(target, domain string) string {
   301  	// Linode always wants a fully qualified target name
   302  	if target[len(target)-1] == '.' {
   303  		return target[:len(target)-1]
   304  	}
   305  	return fmt.Sprintf("%s.%s", target, domain)
   306  }
   307  
   308  func fixTTL(ttl uint32) uint32 {
   309  	// if the TTL is larger than the largest allowed value, return the largest allowed value
   310  	if ttl > allowedTTLValues[len(allowedTTLValues)-1] {
   311  		return allowedTTLValues[len(allowedTTLValues)-1]
   312  	}
   313  
   314  	for _, v := range allowedTTLValues {
   315  		if v >= ttl {
   316  			return v
   317  		}
   318  	}
   319  
   320  	return allowedTTLValues[0]
   321  }