dnscontrol/providers/dnsimple/dnsimpleProvider.go

337 lines
8.2 KiB
Go

package dnsimple
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
"github.com/miekg/dns/dnsutil"
dnsimpleapi "github.com/dnsimple/dnsimple-go/dnsimple"
)
const stateRegistered = "registered"
var defaultNameServerNames = []string{
"ns1.dnsimple.com",
"ns2.dnsimple.com",
"ns3.dnsimple.com",
"ns4.dnsimple.com",
}
type DnsimpleApi struct {
AccountToken string // The account access token
BaseURL string // An alternate base URI
accountId string // Account id cache
}
func (c *DnsimpleApi) GetNameservers(domainName string) ([]*models.Nameserver, error) {
return models.StringsToNameservers(defaultNameServerNames), nil
}
func (c *DnsimpleApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
corrections := []*models.Correction{}
records, err := c.getRecords(dc.Name)
if err != nil {
return nil, err
}
var actual []*models.RecordConfig
for _, r := range records {
if r.Type == "SOA" || r.Type == "NS" {
continue
}
if r.Name == "" {
r.Name = "@"
}
if r.Type == "CNAME" || r.Type == "MX" {
r.Content += "."
}
rec := &models.RecordConfig{
NameFQDN: dnsutil.AddOrigin(r.Name, dc.Name),
Type: r.Type,
Target: r.Content,
TTL: uint32(r.TTL),
Priority: uint16(r.Priority),
Original: r,
}
actual = append(actual, rec)
}
removeOtherNS(dc)
differ := diff.New(dc)
_, create, delete, modify := differ.IncrementalDiff(actual)
for _, del := range delete {
rec := del.Existing.Original.(dnsimpleapi.ZoneRecord)
corrections = append(corrections, &models.Correction{
Msg: del.String(),
F: c.deleteRecordFunc(rec.ID, dc.Name),
})
}
for _, cre := range create {
rec := cre.Desired
corrections = append(corrections, &models.Correction{
Msg: cre.String(),
F: c.createRecordFunc(rec, dc.Name),
})
}
for _, mod := range modify {
old := mod.Existing.Original.(dnsimpleapi.ZoneRecord)
new := mod.Desired
corrections = append(corrections, &models.Correction{
Msg: mod.String(),
F: c.updateRecordFunc(&old, new, dc.Name),
})
}
return corrections, nil
}
func (c *DnsimpleApi) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
corrections := []*models.Correction{}
nameServers, err := c.getNameservers(dc.Name)
if err != nil {
return nil, err
}
actual := strings.Join(nameServers, ",")
expectedSet := []string{}
for _, ns := range dc.Nameservers {
expectedSet = append(expectedSet, ns.Name)
}
sort.Strings(expectedSet)
expected := strings.Join(expectedSet, ",")
if actual != expected {
return []*models.Correction{
{
Msg: fmt.Sprintf("Update nameservers %s -> %s", actual, expected),
F: c.updateNameserversFunc(expectedSet, dc.Name),
},
}, nil
}
return corrections, nil
}
// DNSimple calls
func (c *DnsimpleApi) getClient() *dnsimpleapi.Client {
client := dnsimpleapi.NewClient(dnsimpleapi.NewOauthTokenCredentials(c.AccountToken))
if c.BaseURL != "" {
client.BaseURL = c.BaseURL
}
return client
}
func (c *DnsimpleApi) getAccountId() (string, error) {
if c.accountId == "" {
client := c.getClient()
whoamiResponse, err := client.Identity.Whoami()
if err != nil {
return "", err
}
if whoamiResponse.Data.User != nil && whoamiResponse.Data.Account == nil {
return "", fmt.Errorf("DNSimple token appears to be a user token. Please supply an account token")
}
c.accountId = strconv.Itoa(whoamiResponse.Data.Account.ID)
}
return c.accountId, nil
}
func (c *DnsimpleApi) getRecords(domainName string) ([]dnsimpleapi.ZoneRecord, error) {
client := c.getClient()
accountId, err := c.getAccountId()
if err != nil {
return nil, err
}
recordsResponse, err := client.Zones.ListRecords(accountId, domainName, nil)
if err != nil {
return nil, err
}
return recordsResponse.Data, nil
}
// Returns the name server names that should be used. If the domain is registered
// then this method will return the delegation name servers. If this domain
// is hosted only, then it will return the default DNSimple name servers.
func (c *DnsimpleApi) getNameservers(domainName string) ([]string, error) {
client := c.getClient()
accountId, err := c.getAccountId()
if err != nil {
return nil, err
}
domainResponse, err := client.Domains.GetDomain(accountId, domainName)
if err != nil {
return nil, err
}
if domainResponse.Data.State == stateRegistered {
delegationResponse, err := client.Registrar.GetDomainDelegation(accountId, domainName)
if err != nil {
return nil, err
}
return *delegationResponse.Data, nil
} else {
return defaultNameServerNames, nil
}
}
// Returns a function that can be invoked to change the delegation of the domain to the given name server names.
func (c *DnsimpleApi) updateNameserversFunc(nameServerNames []string, domainName string) func() error {
return func() error {
client := c.getClient()
accountId, err := c.getAccountId()
if err != nil {
return err
}
nameServers := dnsimpleapi.Delegation(nameServerNames)
_, err = client.Registrar.ChangeDomainDelegation(accountId, domainName, &nameServers)
if err != nil {
return err
}
return nil
}
}
// Returns a function that can be invoked to create a record in a zone.
func (c *DnsimpleApi) createRecordFunc(rc *models.RecordConfig, domainName string) func() error {
return func() error {
client := c.getClient()
accountId, err := c.getAccountId()
if err != nil {
return err
}
record := dnsimpleapi.ZoneRecord{
Name: dnsutil.TrimDomainName(rc.NameFQDN, domainName),
Type: rc.Type,
Content: rc.Target,
TTL: int(rc.TTL),
Priority: int(rc.Priority),
}
_, err = client.Zones.CreateRecord(accountId, domainName, record)
if err != nil {
return err
}
return nil
}
}
// Returns a function that can be invoked to delete a record in a zone.
func (c *DnsimpleApi) deleteRecordFunc(recordId int, domainName string) func() error {
return func() error {
client := c.getClient()
accountId, err := c.getAccountId()
if err != nil {
return err
}
_, err = client.Zones.DeleteRecord(accountId, domainName, recordId)
if err != nil {
return err
}
return nil
}
}
// Returns a function that can be invoked to update a record in a zone.
func (c *DnsimpleApi) updateRecordFunc(old *dnsimpleapi.ZoneRecord, rc *models.RecordConfig, domainName string) func() error {
return func() error {
client := c.getClient()
accountId, err := c.getAccountId()
if err != nil {
return err
}
record := dnsimpleapi.ZoneRecord{
Name: dnsutil.TrimDomainName(rc.NameFQDN, domainName),
Type: rc.Type,
Content: rc.Target,
TTL: int(rc.TTL),
Priority: int(rc.Priority),
}
_, err = client.Zones.UpdateRecord(accountId, domainName, old.ID, record)
if err != nil {
return err
}
return nil
}
}
// constructors
func newReg(conf map[string]string) (providers.Registrar, error) {
return newProvider(conf, nil)
}
func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
return newProvider(conf, metadata)
}
func newProvider(m map[string]string, metadata json.RawMessage) (*DnsimpleApi, error) {
api := &DnsimpleApi{}
api.AccountToken = m["token"]
if api.AccountToken == "" {
return nil, fmt.Errorf("DNSimple token must be provided.")
}
if m["baseurl"] != "" {
api.BaseURL = m["baseurl"]
}
return api, nil
}
func init() {
providers.RegisterRegistrarType("DNSIMPLE", newReg)
providers.RegisterDomainServiceProviderType("DNSIMPLE", newDsp)
}
// remove all non-dnsimple NS records from our desired state.
// if any are found, print a warning
func removeOtherNS(dc *models.DomainConfig) {
newList := make([]*models.RecordConfig, 0, len(dc.Records))
for _, rec := range dc.Records {
if rec.Type == "NS" {
// apex NS inside dnsimple are expected.
if rec.NameFQDN == dc.Name && strings.HasSuffix(rec.Target, ".dnsimple.com.") {
continue
}
fmt.Printf("Warning: dnsimple.com does not allow NS records to be modified. %s will not be added.\n", rec.Target)
continue
}
newList = append(newList, rec)
}
dc.Records = newList
}