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

     1  package vultr
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     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  	vultr "github.com/JamesClonk/vultr/lib"
    16  )
    17  
    18  /*
    19  
    20  Vultr API DNS provider:
    21  
    22  Info required in `creds.json`:
    23     - token
    24  
    25  */
    26  
    27  var docNotes = providers.DocumentationNotes{
    28  	providers.DocCreateDomains:       providers.Can(),
    29  	providers.DocOfficiallySupported: providers.Cannot(),
    30  	providers.CanUseAlias:            providers.Cannot(),
    31  	providers.CanUseTLSA:             providers.Cannot(),
    32  	providers.CanUsePTR:              providers.Cannot(),
    33  }
    34  
    35  func init() {
    36  	providers.RegisterDomainServiceProviderType("VULTR", NewVultr, providers.CanUseSRV, providers.CanUseCAA, docNotes)
    37  }
    38  
    39  // VultrApi represents the Vultr DNSServiceProvider
    40  type VultrApi struct {
    41  	client *vultr.Client
    42  	token  string
    43  }
    44  
    45  // defaultNS are the default nameservers for Vultr
    46  var defaultNS = []string{
    47  	"ns1.vultr.com",
    48  	"ns2.vultr.com",
    49  }
    50  
    51  // NewVultr initializes a Vultr DNSServiceProvider
    52  func NewVultr(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
    53  	api := &VultrApi{
    54  		token: m["token"],
    55  	}
    56  
    57  	if api.token == "" {
    58  		return nil, fmt.Errorf("Vultr API token is required")
    59  	}
    60  
    61  	api.client = vultr.NewClient(api.token, nil)
    62  
    63  	// Validate token
    64  	_, err := api.client.GetAccountInfo()
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	return api, nil
    70  }
    71  
    72  // GetDomainCorrections gets the corrections for a DomainConfig
    73  func (api *VultrApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
    74  	dc.Punycode()
    75  
    76  	ok, err := api.isDomainInAccount(dc.Name)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	if !ok {
    82  		return nil, fmt.Errorf("%s is not a domain in the Vultr account", dc.Name)
    83  	}
    84  
    85  	records, err := api.client.GetDNSRecords(dc.Name)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	curRecords := make([]*models.RecordConfig, len(records))
    91  	for i := range records {
    92  		r, err := toRecordConfig(dc, &records[i])
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  
    97  		curRecords[i] = r
    98  	}
    99  
   100  	differ := diff.New(dc)
   101  	_, create, delete, modify := differ.IncrementalDiff(curRecords)
   102  
   103  	corrections := []*models.Correction{}
   104  
   105  	for _, mod := range delete {
   106  		id := mod.Existing.Original.(*vultr.DNSRecord).RecordID
   107  		corrections = append(corrections, &models.Correction{
   108  			Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id),
   109  			F: func() error {
   110  				return api.client.DeleteDNSRecord(dc.Name, id)
   111  			},
   112  		})
   113  	}
   114  
   115  	for _, mod := range create {
   116  		r := toVultrRecord(dc, mod.Desired)
   117  		corrections = append(corrections, &models.Correction{
   118  			Msg: mod.String(),
   119  			F: func() error {
   120  				return api.client.CreateDNSRecord(dc.Name, r.Name, r.Type, r.Data, r.Priority, r.TTL)
   121  			},
   122  		})
   123  	}
   124  
   125  	for _, mod := range modify {
   126  		id := mod.Existing.Original.(*vultr.DNSRecord).RecordID
   127  		r := toVultrRecord(dc, mod.Desired)
   128  		r.RecordID = id
   129  		corrections = append(corrections, &models.Correction{
   130  			Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id),
   131  			F: func() error {
   132  				return api.client.UpdateDNSRecord(dc.Name, *r)
   133  			},
   134  		})
   135  	}
   136  
   137  	return corrections, nil
   138  }
   139  
   140  // GetNameservers gets the Vultr nameservers for a domain
   141  func (api *VultrApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
   142  	return models.StringsToNameservers(defaultNS), nil
   143  }
   144  
   145  // EnsureDomainExists adds a domain to the Vutr DNS service if it does not exist
   146  func (api *VultrApi) EnsureDomainExists(domain string) error {
   147  	ok, err := api.isDomainInAccount(domain)
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	if !ok {
   153  		// Vultr requires an initial IP, use a dummy one
   154  		err := api.client.CreateDNSDomain(domain, "127.0.0.1")
   155  		if err != nil {
   156  			return err
   157  		}
   158  
   159  		ok, err := api.isDomainInAccount(domain)
   160  		if err != nil {
   161  			return err
   162  		}
   163  		if !ok {
   164  			return fmt.Errorf("Unexpected error adding domain %s to Vultr account", domain)
   165  		}
   166  	}
   167  
   168  	return nil
   169  }
   170  
   171  func (api *VultrApi) isDomainInAccount(domain string) (bool, error) {
   172  	domains, err := api.client.GetDNSDomains()
   173  	if err != nil {
   174  		return false, err
   175  	}
   176  
   177  	var vd *vultr.DNSDomain
   178  	for _, d := range domains {
   179  		if d.Domain == domain {
   180  			vd = &d
   181  		}
   182  	}
   183  
   184  	if vd == nil {
   185  		return false, nil
   186  	}
   187  
   188  	return true, nil
   189  }
   190  
   191  // toRecordConfig converts a Vultr DNSRecord to a RecordConfig #rtype_variations
   192  func toRecordConfig(dc *models.DomainConfig, r *vultr.DNSRecord) (*models.RecordConfig, error) {
   193  	// Turns r.Name into a FQDN
   194  	// Vultr uses "" as the apex domain, instead of "@", and this handles it fine.
   195  	name := dnsutil.AddOrigin(r.Name, dc.Name)
   196  
   197  	data := r.Data
   198  	// Make target into a FQDN if it is a CNAME, NS, MX, or SRV
   199  	if r.Type == "CNAME" || r.Type == "NS" || r.Type == "MX" {
   200  		if !strings.HasSuffix(data, ".") {
   201  			data = data + "."
   202  		}
   203  		data = dnsutil.AddOrigin(data, dc.Name)
   204  	}
   205  	// Remove quotes if it is a TXT
   206  	if r.Type == "TXT" {
   207  		if !strings.HasPrefix(data, `"`) || !strings.HasSuffix(data, `"`) {
   208  			return nil, errors.New("Unexpected lack of quotes in TXT record from Vultr")
   209  		}
   210  		data = data[1 : len(data)-1]
   211  	}
   212  
   213  	rc := &models.RecordConfig{
   214  		NameFQDN: name,
   215  		Type:     r.Type,
   216  		Target:   data,
   217  		TTL:      uint32(r.TTL),
   218  		Original: r,
   219  	}
   220  
   221  	if r.Type == "MX" {
   222  		rc.MxPreference = uint16(r.Priority)
   223  	}
   224  
   225  	if r.Type == "SRV" {
   226  		rc.SrvPriority = uint16(r.Priority)
   227  
   228  		// Vultr returns in the format "[weight] [port] [target]"
   229  		splitData := strings.SplitN(rc.Target, " ", 3)
   230  		if len(splitData) != 3 {
   231  			return nil, fmt.Errorf("Unexpected data for SRV record returned by Vultr")
   232  		}
   233  
   234  		weight, err := strconv.ParseUint(splitData[0], 10, 16)
   235  		if err != nil {
   236  			return nil, err
   237  		}
   238  		rc.SrvWeight = uint16(weight)
   239  
   240  		port, err := strconv.ParseUint(splitData[1], 10, 16)
   241  		if err != nil {
   242  			return nil, err
   243  		}
   244  		rc.SrvPort = uint16(port)
   245  
   246  		target := splitData[2]
   247  		if !strings.HasSuffix(target, ".") {
   248  			target = target + "."
   249  		}
   250  		rc.Target = dnsutil.AddOrigin(target, dc.Name)
   251  	}
   252  
   253  	if r.Type == "CAA" {
   254  		// Vultr returns in the format "[flag] [tag] [value]"
   255  		splitData := strings.SplitN(rc.Target, " ", 3)
   256  		if len(splitData) != 3 {
   257  			return nil, fmt.Errorf("Unexpected data for CAA record returned by Vultr")
   258  		}
   259  
   260  		flag, err := strconv.ParseUint(splitData[0], 10, 8)
   261  		if err != nil {
   262  			return nil, err
   263  		}
   264  		rc.CaaFlag = uint8(flag)
   265  
   266  		rc.CaaTag = splitData[1]
   267  
   268  		value := splitData[2]
   269  		if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) {
   270  			value = value[1 : len(value)-1]
   271  		}
   272  		if strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`) {
   273  			value = value[1 : len(value)-1]
   274  		}
   275  		rc.Target = value
   276  	}
   277  
   278  	return rc, nil
   279  }
   280  
   281  // toVultrRecord converts a RecordConfig converted by toRecordConfig back to a Vultr DNSRecord #rtype_variations
   282  func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig) *vultr.DNSRecord {
   283  	name := dnsutil.TrimDomainName(rc.NameFQDN, dc.Name)
   284  
   285  	// Vultr uses a blank string to represent the apex domain
   286  	if name == "@" {
   287  		name = ""
   288  	}
   289  
   290  	data := rc.Target
   291  
   292  	// Vultr does not use a period suffix for the server for CNAME, NS, or MX
   293  	if strings.HasSuffix(data, ".") {
   294  		data = data[:len(data)-1]
   295  	}
   296  	// Vultr needs TXT record in quotes
   297  	if rc.Type == "TXT" {
   298  		data = fmt.Sprintf(`"%s"`, data)
   299  	}
   300  
   301  	priority := 0
   302  
   303  	if rc.Type == "MX" {
   304  		priority = int(rc.MxPreference)
   305  	}
   306  
   307  	if rc.Type == "SRV" {
   308  		priority = int(rc.SrvPriority)
   309  	}
   310  
   311  	r := &vultr.DNSRecord{
   312  		Type:     rc.Type,
   313  		Name:     name,
   314  		Data:     data,
   315  		TTL:      int(rc.TTL),
   316  		Priority: priority,
   317  	}
   318  
   319  	if rc.Type == "SRV" {
   320  		target := rc.Target
   321  		if strings.HasSuffix(target, ".") {
   322  			target = target[:len(target)-1]
   323  		}
   324  
   325  		r.Data = fmt.Sprintf("%v %v %s", rc.SrvWeight, rc.SrvPort, target)
   326  	}
   327  
   328  	if rc.Type == "CAA" {
   329  		r.Data = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.Target)
   330  	}
   331  
   332  	return r
   333  }