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

     1  package vultr
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/miekg/dns/dnsutil"
    12  	"github.com/vultr/govultr"
    13  
    14  	"github.com/StackExchange/dnscontrol/v2/models"
    15  	"github.com/StackExchange/dnscontrol/v2/providers"
    16  	"github.com/StackExchange/dnscontrol/v2/providers/diff"
    17  )
    18  
    19  /*
    20  
    21  Vultr API DNS provider:
    22  
    23  Info required in `creds.json`:
    24     - token
    25  
    26  */
    27  
    28  var features = providers.DocumentationNotes{
    29  	providers.CanUseAlias:            providers.Cannot(),
    30  	providers.CanUseCAA:              providers.Can(),
    31  	providers.CanUsePTR:              providers.Cannot(),
    32  	providers.CanUseSRV:              providers.Can(),
    33  	providers.CanUseTLSA:             providers.Cannot(),
    34  	providers.CanUseSSHFP:            providers.Can(),
    35  	providers.DocCreateDomains:       providers.Can(),
    36  	providers.DocOfficiallySupported: providers.Cannot(),
    37  	providers.CanGetZones:            providers.Unimplemented(),
    38  }
    39  
    40  func init() {
    41  	providers.RegisterDomainServiceProviderType("VULTR", NewProvider, features)
    42  }
    43  
    44  // Provider represents the Vultr DNSServiceProvider.
    45  type Provider struct {
    46  	client *govultr.Client
    47  	token  string
    48  }
    49  
    50  // defaultNS contains the default nameservers for Vultr.
    51  var defaultNS = []string{
    52  	"ns1.vultr.com",
    53  	"ns2.vultr.com",
    54  }
    55  
    56  // NewProvider initializes a Vultr DNSServiceProvider.
    57  func NewProvider(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
    58  	token := m["token"]
    59  	if token == "" {
    60  		return nil, fmt.Errorf("Vultr API token is required")
    61  	}
    62  
    63  	client := govultr.NewClient(nil, token)
    64  	client.SetUserAgent("dnscontrol")
    65  
    66  	_, err := client.Account.GetInfo(context.Background())
    67  	return &Provider{client, token}, err
    68  }
    69  
    70  // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
    71  func (client *Provider) GetZoneRecords(domain string) (models.Records, error) {
    72  	return nil, fmt.Errorf("not implemented")
    73  	// This enables the get-zones subcommand.
    74  	// Implement this by extracting the code from GetDomainCorrections into
    75  	// a single function.  For most providers this should be relatively easy.
    76  }
    77  
    78  // GetDomainCorrections gets the corrections for a DomainConfig.
    79  func (api *Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
    80  	dc.Punycode()
    81  
    82  	records, err := api.client.DNSRecord.List(context.Background(), dc.Name)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	curRecords := make([]*models.RecordConfig, len(records))
    88  	for i := range records {
    89  		r, err := toRecordConfig(dc, &records[i])
    90  		if err != nil {
    91  			return nil, err
    92  		}
    93  		curRecords[i] = r
    94  	}
    95  
    96  	models.PostProcessRecords(curRecords)
    97  
    98  	differ := diff.New(dc)
    99  	_, create, delete, modify := differ.IncrementalDiff(curRecords)
   100  
   101  	var corrections []*models.Correction
   102  
   103  	for _, mod := range delete {
   104  		id := mod.Existing.Original.(*govultr.DNSRecord).RecordID
   105  		corrections = append(corrections, &models.Correction{
   106  			Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id),
   107  			F: func() error {
   108  				return api.client.DNSRecord.Delete(context.Background(), dc.Name, strconv.Itoa(id))
   109  			},
   110  		})
   111  	}
   112  
   113  	for _, mod := range create {
   114  		r := toVultrRecord(dc, mod.Desired, 0)
   115  		corrections = append(corrections, &models.Correction{
   116  			Msg: mod.String(),
   117  			F: func() error {
   118  				return api.client.DNSRecord.Create(context.Background(), dc.Name, r.Type, r.Name, r.Data, r.TTL, r.Priority)
   119  			},
   120  		})
   121  	}
   122  
   123  	for _, mod := range modify {
   124  		r := toVultrRecord(dc, mod.Desired, mod.Existing.Original.(*govultr.DNSRecord).RecordID)
   125  		corrections = append(corrections, &models.Correction{
   126  			Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), r.RecordID),
   127  			F: func() error {
   128  				return api.client.DNSRecord.Update(context.Background(), dc.Name, r)
   129  			},
   130  		})
   131  	}
   132  
   133  	return corrections, nil
   134  }
   135  
   136  // GetNameservers gets the Vultr nameservers for a domain
   137  func (api *Provider) GetNameservers(domain string) ([]*models.Nameserver, error) {
   138  	return models.StringsToNameservers(defaultNS), nil
   139  }
   140  
   141  // EnsureDomainExists adds a domain to the Vutr DNS service if it does not exist
   142  func (api *Provider) EnsureDomainExists(domain string) error {
   143  	if ok, err := api.isDomainInAccount(domain); err != nil {
   144  		return err
   145  	} else if ok {
   146  		return nil
   147  	}
   148  
   149  	// Vultr requires an initial IP, use a dummy one.
   150  	return api.client.DNSDomain.Create(context.Background(), domain, "0.0.0.0")
   151  }
   152  
   153  func (api *Provider) isDomainInAccount(domain string) (bool, error) {
   154  	domains, err := api.client.DNSDomain.List(context.Background())
   155  	if err != nil {
   156  		return false, err
   157  	}
   158  	for _, d := range domains {
   159  		if d.Domain == domain {
   160  			return true, nil
   161  		}
   162  	}
   163  	return false, nil
   164  }
   165  
   166  // toRecordConfig converts a Vultr DNSRecord to a RecordConfig. #rtype_variations
   167  func toRecordConfig(dc *models.DomainConfig, r *govultr.DNSRecord) (*models.RecordConfig, error) {
   168  	origin, data := dc.Name, r.Data
   169  	rc := &models.RecordConfig{
   170  		TTL:      uint32(r.TTL),
   171  		Original: r,
   172  	}
   173  	rc.SetLabel(r.Name, dc.Name)
   174  
   175  	switch rtype := r.Type; rtype {
   176  	case "CNAME", "NS":
   177  		rc.Type = r.Type
   178  		// Make target into a FQDN if it is a CNAME, NS, MX, or SRV.
   179  		if !strings.HasSuffix(data, ".") {
   180  			data = data + "."
   181  		}
   182  		// FIXME(tlim): the AddOrigin() might be unneeded. Please test.
   183  		return rc, rc.SetTarget(dnsutil.AddOrigin(data, origin))
   184  	case "CAA":
   185  		// Vultr returns CAA records in the format "[flag] [tag] [value]".
   186  		return rc, rc.SetTargetCAAString(data)
   187  	case "MX":
   188  		if !strings.HasSuffix(data, ".") {
   189  			data = data + "."
   190  		}
   191  		return rc, rc.SetTargetMX(uint16(r.Priority), data)
   192  	case "SRV":
   193  		// Vultr returns SRV records in the format "[weight] [port] [target]".
   194  		return rc, rc.SetTargetSRVPriorityString(uint16(r.Priority), data)
   195  	case "TXT":
   196  		// Remove quotes if it is a TXT record.
   197  		if !strings.HasPrefix(data, `"`) || !strings.HasSuffix(data, `"`) {
   198  			return nil, errors.New("Unexpected lack of quotes in TXT record from Vultr")
   199  		}
   200  		return rc, rc.SetTargetTXT(data[1 : len(data)-1])
   201  	default:
   202  		return rc, rc.PopulateFromString(rtype, r.Data, origin)
   203  	}
   204  }
   205  
   206  // toVultrRecord converts a RecordConfig converted by toRecordConfig back to a Vultr DNSRecord. #rtype_variations
   207  func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig, vultrID int) *govultr.DNSRecord {
   208  	name := rc.GetLabel()
   209  	// Vultr uses a blank string to represent the apex domain.
   210  	if name == "@" {
   211  		name = ""
   212  	}
   213  
   214  	data := rc.GetTargetField()
   215  
   216  	// Vultr does not use a period suffix for CNAME, NS, or MX.
   217  	if strings.HasSuffix(data, ".") {
   218  		data = data[:len(data)-1]
   219  	}
   220  	// Vultr needs TXT record in quotes.
   221  	if rc.Type == "TXT" {
   222  		data = fmt.Sprintf(`"%s"`, data)
   223  	}
   224  
   225  	priority := 0
   226  
   227  	if rc.Type == "MX" {
   228  		priority = int(rc.MxPreference)
   229  	}
   230  	if rc.Type == "SRV" {
   231  		priority = int(rc.SrvPriority)
   232  	}
   233  
   234  	r := &govultr.DNSRecord{
   235  		RecordID: vultrID,
   236  		Type:     rc.Type,
   237  		Name:     name,
   238  		Data:     data,
   239  		TTL:      int(rc.TTL),
   240  		Priority: priority,
   241  	}
   242  	switch rtype := rc.Type; rtype { // #rtype_variations
   243  	case "SRV":
   244  		r.Data = fmt.Sprintf("%v %v %s", rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
   245  	case "CAA":
   246  		r.Data = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
   247  	case "SSHFP":
   248  		r.Data = fmt.Sprintf("%d %d %s", rc.SshfpAlgorithm, rc.SshfpFingerprint, rc.GetTargetField())
   249  	default:
   250  	}
   251  
   252  	return r
   253  }