mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-12-28 03:04:05 +08:00
337 lines
8.2 KiB
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
|
|
}
|