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

     1  package vultr
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strings"
     7  
     8  	vultr "github.com/JamesClonk/vultr/lib"
     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  
    16  /*
    17  
    18  Vultr API DNS provider:
    19  
    20  Info required in `creds.json`:
    21     - token
    22  
    23  */
    24  
    25  var features = providers.DocumentationNotes{
    26  	providers.CanUseAlias:            providers.Cannot(),
    27  	providers.CanUseCAA:              providers.Can(),
    28  	providers.CanUsePTR:              providers.Cannot(),
    29  	providers.CanUseSRV:              providers.Can(),
    30  	providers.CanUseTLSA:             providers.Cannot(),
    31  	providers.DocCreateDomains:       providers.Can(),
    32  	providers.DocOfficiallySupported: providers.Cannot(),
    33  }
    34  
    35  func init() {
    36  	providers.RegisterDomainServiceProviderType("VULTR", NewVultr, features)
    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, errors.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, errors.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  	// Normalize
   101  	models.PostProcessRecords(curRecords)
   102  
   103  	differ := diff.New(dc)
   104  	_, create, delete, modify := differ.IncrementalDiff(curRecords)
   105  
   106  	corrections := []*models.Correction{}
   107  
   108  	for _, mod := range delete {
   109  		id := mod.Existing.Original.(*vultr.DNSRecord).RecordID
   110  		corrections = append(corrections, &models.Correction{
   111  			Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id),
   112  			F: func() error {
   113  				return api.client.DeleteDNSRecord(dc.Name, id)
   114  			},
   115  		})
   116  	}
   117  
   118  	for _, mod := range create {
   119  		r := toVultrRecord(dc, mod.Desired)
   120  		corrections = append(corrections, &models.Correction{
   121  			Msg: mod.String(),
   122  			F: func() error {
   123  				return api.client.CreateDNSRecord(dc.Name, r.Name, r.Type, r.Data, r.Priority, r.TTL)
   124  			},
   125  		})
   126  	}
   127  
   128  	for _, mod := range modify {
   129  		id := mod.Existing.Original.(*vultr.DNSRecord).RecordID
   130  		r := toVultrRecord(dc, mod.Desired)
   131  		r.RecordID = id
   132  		corrections = append(corrections, &models.Correction{
   133  			Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id),
   134  			F: func() error {
   135  				return api.client.UpdateDNSRecord(dc.Name, *r)
   136  			},
   137  		})
   138  	}
   139  
   140  	return corrections, nil
   141  }
   142  
   143  // GetNameservers gets the Vultr nameservers for a domain
   144  func (api *VultrApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
   145  	return models.StringsToNameservers(defaultNS), nil
   146  }
   147  
   148  // EnsureDomainExists adds a domain to the Vutr DNS service if it does not exist
   149  func (api *VultrApi) EnsureDomainExists(domain string) error {
   150  	ok, err := api.isDomainInAccount(domain)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	if !ok {
   156  		// Vultr requires an initial IP, use a dummy one
   157  		err := api.client.CreateDNSDomain(domain, "127.0.0.1")
   158  		if err != nil {
   159  			return err
   160  		}
   161  
   162  		ok, err := api.isDomainInAccount(domain)
   163  		if err != nil {
   164  			return err
   165  		}
   166  		if !ok {
   167  			return errors.Errorf("Unexpected error adding domain %s to Vultr account", domain)
   168  		}
   169  	}
   170  
   171  	return nil
   172  }
   173  
   174  func (api *VultrApi) isDomainInAccount(domain string) (bool, error) {
   175  	domains, err := api.client.GetDNSDomains()
   176  	if err != nil {
   177  		return false, err
   178  	}
   179  
   180  	var vd *vultr.DNSDomain
   181  	for _, d := range domains {
   182  		if d.Domain == domain {
   183  			vd = &d
   184  		}
   185  	}
   186  
   187  	if vd == nil {
   188  		return false, nil
   189  	}
   190  
   191  	return true, nil
   192  }
   193  
   194  // toRecordConfig converts a Vultr DNSRecord to a RecordConfig #rtype_variations
   195  func toRecordConfig(dc *models.DomainConfig, r *vultr.DNSRecord) (*models.RecordConfig, error) {
   196  	origin := dc.Name
   197  	data := r.Data
   198  	rc := &models.RecordConfig{
   199  		TTL:      uint32(r.TTL),
   200  		Original: r,
   201  	}
   202  	rc.SetLabel(r.Name, dc.Name)
   203  
   204  	switch rtype := r.Type; rtype {
   205  	case "CNAME", "NS":
   206  		rc.Type = r.Type
   207  		// Make target into a FQDN if it is a CNAME, NS, MX, or SRV
   208  		if !strings.HasSuffix(data, ".") {
   209  			data = data + "."
   210  		}
   211  		// FIXME(tlim): the AddOrigin() might be unneeded. Please test.
   212  		return rc, rc.SetTarget(dnsutil.AddOrigin(data, origin))
   213  	case "CAA":
   214  		// Vultr returns in the format "[flag] [tag] [value]"
   215  		return rc, rc.SetTargetCAAString(data)
   216  	case "MX":
   217  		if !strings.HasSuffix(data, ".") {
   218  			data = data + "."
   219  		}
   220  		return rc, rc.SetTargetMX(uint16(r.Priority), data)
   221  	case "SRV":
   222  		// Vultr returns in the format "[weight] [port] [target]"
   223  		return rc, rc.SetTargetSRVPriorityString(uint16(r.Priority), data)
   224  	case "TXT":
   225  		// Remove quotes if it is a TXT
   226  		if !strings.HasPrefix(data, `"`) || !strings.HasSuffix(data, `"`) {
   227  			return nil, errors.New("Unexpected lack of quotes in TXT record from Vultr")
   228  		}
   229  		return rc, rc.SetTargetTXT(data[1 : len(data)-1])
   230  	default:
   231  		return rc, rc.PopulateFromString(rtype, r.Data, origin)
   232  	}
   233  }
   234  
   235  // toVultrRecord converts a RecordConfig converted by toRecordConfig back to a Vultr DNSRecord #rtype_variations
   236  func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig) *vultr.DNSRecord {
   237  	name := rc.GetLabel()
   238  	// Vultr uses a blank string to represent the apex domain
   239  	if name == "@" {
   240  		name = ""
   241  	}
   242  
   243  	data := rc.GetTargetField()
   244  
   245  	// Vultr does not use a period suffix for the server for CNAME, NS, or MX
   246  	if strings.HasSuffix(data, ".") {
   247  		data = data[:len(data)-1]
   248  	}
   249  	// Vultr needs TXT record in quotes
   250  	if rc.Type == "TXT" {
   251  		data = fmt.Sprintf(`"%s"`, data)
   252  	}
   253  
   254  	priority := 0
   255  
   256  	if rc.Type == "MX" {
   257  		priority = int(rc.MxPreference)
   258  	}
   259  	if rc.Type == "SRV" {
   260  		priority = int(rc.SrvPriority)
   261  	}
   262  
   263  	r := &vultr.DNSRecord{
   264  		Type:     rc.Type,
   265  		Name:     name,
   266  		Data:     data,
   267  		TTL:      int(rc.TTL),
   268  		Priority: priority,
   269  	}
   270  	switch rtype := rc.Type; rtype { // #rtype_variations
   271  	case "SRV":
   272  		target := rc.GetTargetField()
   273  		if strings.HasSuffix(target, ".") {
   274  			target = target[:len(target)-1]
   275  		}
   276  		r.Data = fmt.Sprintf("%v %v %s", rc.SrvWeight, rc.SrvPort, target)
   277  	case "CAA":
   278  		r.Data = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
   279  	default:
   280  	}
   281  
   282  	return r
   283  }