dnscontrol/providers/vultr/vultrProvider.go
Tom Limoncelli a7eba97ada
Refactor in preparation to unexport RecordConfig.{Name,NameFQDN,Target} (#337)
* Preparing for the unexport of Name/NameFQDN/Target
* Cleanups
2018-03-19 17:18:58 -04:00

284 lines
6.8 KiB
Go

package vultr
import (
"encoding/json"
"fmt"
"strings"
vultr "github.com/JamesClonk/vultr/lib"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
"github.com/miekg/dns/dnsutil"
"github.com/pkg/errors"
)
/*
Vultr API DNS provider:
Info required in `creds.json`:
- token
*/
var features = providers.DocumentationNotes{
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUsePTR: providers.Cannot(),
providers.CanUseSRV: providers.Can(),
providers.CanUseTLSA: providers.Cannot(),
providers.DocCreateDomains: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
}
func init() {
providers.RegisterDomainServiceProviderType("VULTR", NewVultr, features)
}
// VultrApi represents the Vultr DNSServiceProvider
type VultrApi struct {
client *vultr.Client
token string
}
// defaultNS are the default nameservers for Vultr
var defaultNS = []string{
"ns1.vultr.com",
"ns2.vultr.com",
}
// NewVultr initializes a Vultr DNSServiceProvider
func NewVultr(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
api := &VultrApi{
token: m["token"],
}
if api.token == "" {
return nil, errors.Errorf("Vultr API token is required")
}
api.client = vultr.NewClient(api.token, nil)
// Validate token
_, err := api.client.GetAccountInfo()
if err != nil {
return nil, err
}
return api, nil
}
// GetDomainCorrections gets the corrections for a DomainConfig
func (api *VultrApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
ok, err := api.isDomainInAccount(dc.Name)
if err != nil {
return nil, err
}
if !ok {
return nil, errors.Errorf("%s is not a domain in the Vultr account", dc.Name)
}
records, err := api.client.GetDNSRecords(dc.Name)
if err != nil {
return nil, err
}
curRecords := make([]*models.RecordConfig, len(records))
for i := range records {
r, err := toRecordConfig(dc, &records[i])
if err != nil {
return nil, err
}
curRecords[i] = r
}
// Normalize
models.PostProcessRecords(curRecords)
differ := diff.New(dc)
_, create, delete, modify := differ.IncrementalDiff(curRecords)
corrections := []*models.Correction{}
for _, mod := range delete {
id := mod.Existing.Original.(*vultr.DNSRecord).RecordID
corrections = append(corrections, &models.Correction{
Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id),
F: func() error {
return api.client.DeleteDNSRecord(dc.Name, id)
},
})
}
for _, mod := range create {
r := toVultrRecord(dc, mod.Desired)
corrections = append(corrections, &models.Correction{
Msg: mod.String(),
F: func() error {
return api.client.CreateDNSRecord(dc.Name, r.Name, r.Type, r.Data, r.Priority, r.TTL)
},
})
}
for _, mod := range modify {
id := mod.Existing.Original.(*vultr.DNSRecord).RecordID
r := toVultrRecord(dc, mod.Desired)
r.RecordID = id
corrections = append(corrections, &models.Correction{
Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id),
F: func() error {
return api.client.UpdateDNSRecord(dc.Name, *r)
},
})
}
return corrections, nil
}
// GetNameservers gets the Vultr nameservers for a domain
func (api *VultrApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
return models.StringsToNameservers(defaultNS), nil
}
// EnsureDomainExists adds a domain to the Vutr DNS service if it does not exist
func (api *VultrApi) EnsureDomainExists(domain string) error {
ok, err := api.isDomainInAccount(domain)
if err != nil {
return err
}
if !ok {
// Vultr requires an initial IP, use a dummy one
err := api.client.CreateDNSDomain(domain, "127.0.0.1")
if err != nil {
return err
}
ok, err := api.isDomainInAccount(domain)
if err != nil {
return err
}
if !ok {
return errors.Errorf("Unexpected error adding domain %s to Vultr account", domain)
}
}
return nil
}
func (api *VultrApi) isDomainInAccount(domain string) (bool, error) {
domains, err := api.client.GetDNSDomains()
if err != nil {
return false, err
}
var vd *vultr.DNSDomain
for _, d := range domains {
if d.Domain == domain {
vd = &d
}
}
if vd == nil {
return false, nil
}
return true, nil
}
// toRecordConfig converts a Vultr DNSRecord to a RecordConfig #rtype_variations
func toRecordConfig(dc *models.DomainConfig, r *vultr.DNSRecord) (*models.RecordConfig, error) {
origin := dc.Name
data := r.Data
rc := &models.RecordConfig{
TTL: uint32(r.TTL),
Original: r,
}
rc.SetLabel(r.Name, dc.Name)
switch rtype := r.Type; rtype {
case "CNAME", "NS":
rc.Type = r.Type
// Make target into a FQDN if it is a CNAME, NS, MX, or SRV
if !strings.HasSuffix(data, ".") {
data = data + "."
}
// FIXME(tlim): the AddOrigin() might be unneeded. Please test.
return rc, rc.SetTarget(dnsutil.AddOrigin(data, origin))
case "CAA":
// Vultr returns in the format "[flag] [tag] [value]"
return rc, rc.SetTargetCAAString(data)
case "MX":
if !strings.HasSuffix(data, ".") {
data = data + "."
}
return rc, rc.SetTargetMX(uint16(r.Priority), data)
case "SRV":
// Vultr returns in the format "[weight] [port] [target]"
return rc, rc.SetTargetSRVPriorityString(uint16(r.Priority), data)
case "TXT":
// Remove quotes if it is a TXT
if !strings.HasPrefix(data, `"`) || !strings.HasSuffix(data, `"`) {
return nil, errors.New("Unexpected lack of quotes in TXT record from Vultr")
}
return rc, rc.SetTargetTXT(data[1 : len(data)-1])
default:
return rc, rc.PopulateFromString(rtype, r.Data, origin)
}
}
// toVultrRecord converts a RecordConfig converted by toRecordConfig back to a Vultr DNSRecord #rtype_variations
func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig) *vultr.DNSRecord {
name := rc.GetLabel()
// Vultr uses a blank string to represent the apex domain
if name == "@" {
name = ""
}
data := rc.GetTargetField()
// Vultr does not use a period suffix for the server for CNAME, NS, or MX
if strings.HasSuffix(data, ".") {
data = data[:len(data)-1]
}
// Vultr needs TXT record in quotes
if rc.Type == "TXT" {
data = fmt.Sprintf(`"%s"`, data)
}
priority := 0
if rc.Type == "MX" {
priority = int(rc.MxPreference)
}
if rc.Type == "SRV" {
priority = int(rc.SrvPriority)
}
r := &vultr.DNSRecord{
Type: rc.Type,
Name: name,
Data: data,
TTL: int(rc.TTL),
Priority: priority,
}
switch rtype := rc.Type; rtype { // #rtype_variations
case "SRV":
target := rc.GetTargetField()
if strings.HasSuffix(target, ".") {
target = target[:len(target)-1]
}
r.Data = fmt.Sprintf("%v %v %s", rc.SrvWeight, rc.SrvPort, target)
case "CAA":
r.Data = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
default:
}
return r
}