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

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