mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-12 02:17:43 +08:00
258 lines
7 KiB
Go
258 lines
7 KiB
Go
package vultr
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/miekg/dns/dnsutil"
|
|
"github.com/vultr/govultr"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
|
"github.com/StackExchange/dnscontrol/v3/providers"
|
|
)
|
|
|
|
/*
|
|
|
|
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.CanUseSSHFP: providers.Can(),
|
|
providers.DocCreateDomains: providers.Can(),
|
|
providers.DocOfficiallySupported: providers.Cannot(),
|
|
providers.CanGetZones: providers.Can(),
|
|
}
|
|
|
|
func init() {
|
|
providers.RegisterDomainServiceProviderType("VULTR", NewProvider, features)
|
|
}
|
|
|
|
// Provider represents the Vultr DNSServiceProvider.
|
|
type Provider struct {
|
|
client *govultr.Client
|
|
token string
|
|
}
|
|
|
|
// defaultNS contains the default nameservers for Vultr.
|
|
var defaultNS = []string{
|
|
"ns1.vultr.com",
|
|
"ns2.vultr.com",
|
|
}
|
|
|
|
// NewProvider initializes a Vultr DNSServiceProvider.
|
|
func NewProvider(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
|
token := m["token"]
|
|
if token == "" {
|
|
return nil, fmt.Errorf("Vultr API token is required")
|
|
}
|
|
|
|
client := govultr.NewClient(nil, token)
|
|
client.SetUserAgent("dnscontrol")
|
|
|
|
_, err := client.Account.GetInfo(context.Background())
|
|
return &Provider{client, token}, err
|
|
}
|
|
|
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
|
func (api *Provider) GetZoneRecords(domain string) (models.Records, error) {
|
|
records, err := api.client.DNSRecord.List(context.Background(), domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
curRecords := make(models.Records, len(records))
|
|
for i := range records {
|
|
r, err := toRecordConfig(domain, &records[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
curRecords[i] = r
|
|
}
|
|
|
|
return curRecords, nil
|
|
}
|
|
|
|
// GetDomainCorrections gets the corrections for a DomainConfig.
|
|
func (api *Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
dc.Punycode()
|
|
|
|
curRecords, err := api.GetZoneRecords(dc.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
models.PostProcessRecords(curRecords)
|
|
|
|
differ := diff.New(dc)
|
|
_, create, delete, modify, err := differ.IncrementalDiff(curRecords)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var corrections []*models.Correction
|
|
|
|
for _, mod := range delete {
|
|
id := mod.Existing.Original.(*govultr.DNSRecord).RecordID
|
|
corrections = append(corrections, &models.Correction{
|
|
Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id),
|
|
F: func() error {
|
|
return api.client.DNSRecord.Delete(context.Background(), dc.Name, strconv.Itoa(id))
|
|
},
|
|
})
|
|
}
|
|
|
|
for _, mod := range create {
|
|
r := toVultrRecord(dc, mod.Desired, 0)
|
|
corrections = append(corrections, &models.Correction{
|
|
Msg: mod.String(),
|
|
F: func() error {
|
|
return api.client.DNSRecord.Create(context.Background(), dc.Name, r.Type, r.Name, r.Data, r.TTL, r.Priority)
|
|
},
|
|
})
|
|
}
|
|
|
|
for _, mod := range modify {
|
|
r := toVultrRecord(dc, mod.Desired, mod.Existing.Original.(*govultr.DNSRecord).RecordID)
|
|
corrections = append(corrections, &models.Correction{
|
|
Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), r.RecordID),
|
|
F: func() error {
|
|
return api.client.DNSRecord.Update(context.Background(), dc.Name, r)
|
|
},
|
|
})
|
|
}
|
|
|
|
return corrections, nil
|
|
}
|
|
|
|
// GetNameservers gets the Vultr nameservers for a domain
|
|
func (api *Provider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
|
return models.ToNameservers(defaultNS)
|
|
}
|
|
|
|
// EnsureDomainExists adds a domain to the Vutr DNS service if it does not exist
|
|
func (api *Provider) EnsureDomainExists(domain string) error {
|
|
if ok, err := api.isDomainInAccount(domain); err != nil {
|
|
return err
|
|
} else if ok {
|
|
return nil
|
|
}
|
|
|
|
// Vultr requires an initial IP, use a dummy one.
|
|
return api.client.DNSDomain.Create(context.Background(), domain, "0.0.0.0")
|
|
}
|
|
|
|
func (api *Provider) isDomainInAccount(domain string) (bool, error) {
|
|
domains, err := api.client.DNSDomain.List(context.Background())
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, d := range domains {
|
|
if d.Domain == domain {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// toRecordConfig converts a Vultr DNSRecord to a RecordConfig. #rtype_variations
|
|
func toRecordConfig(domain string, r *govultr.DNSRecord) (*models.RecordConfig, error) {
|
|
origin, data := domain, r.Data
|
|
rc := &models.RecordConfig{
|
|
TTL: uint32(r.TTL),
|
|
Original: r,
|
|
}
|
|
rc.SetLabel(r.Name, domain)
|
|
|
|
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 CAA records 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 SRV records in the format "[weight] [port] [target]".
|
|
return rc, rc.SetTargetSRVPriorityString(uint16(r.Priority), data)
|
|
case "TXT":
|
|
// Remove quotes if it is a TXT record.
|
|
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, vultrID int) *govultr.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 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 := &govultr.DNSRecord{
|
|
RecordID: vultrID,
|
|
Type: rc.Type,
|
|
Name: name,
|
|
Data: data,
|
|
TTL: int(rc.TTL),
|
|
Priority: priority,
|
|
}
|
|
switch rtype := rc.Type; rtype { // #rtype_variations
|
|
case "SRV":
|
|
r.Data = fmt.Sprintf("%v %v %s", rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
|
|
case "CAA":
|
|
r.Data = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
|
|
case "SSHFP":
|
|
r.Data = fmt.Sprintf("%d %d %s", rc.SshfpAlgorithm, rc.SshfpFingerprint, rc.GetTargetField())
|
|
default:
|
|
}
|
|
|
|
return r
|
|
}
|