dnscontrol/providers/transip/transipProvider.go
2025-01-16 14:17:47 -05:00

260 lines
7 KiB
Go

package transip
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/transip/gotransip/v6"
"github.com/transip/gotransip/v6/domain"
"github.com/transip/gotransip/v6/repository"
)
/*
TransIP DNS Provider (transip.nl)
Info required in `creds.json`
- AccessToken
*/
type transipProvider struct {
client *repository.Client
domains *domain.Repository
}
var features = providers.DocumentationNotes{
// The default for unlisted capabilities is 'Cannot'.
// See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanGetZones: providers.Can(),
providers.CanConcur: providers.Can(),
providers.CanUseAKAMAICDN: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
providers.CanUseAzureAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDHCID: providers.Cannot(),
providers.CanUseDNAME: providers.Cannot(),
providers.CanUseDS: providers.Cannot(),
providers.CanUseDSForChildren: providers.Cannot(),
providers.CanUseHTTPS: providers.Cannot(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUseNAPTR: providers.Can(),
providers.CanUsePTR: providers.Cannot(),
providers.CanUseRoute53Alias: providers.Cannot(),
providers.CanUseSOA: providers.Cannot(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
providers.CanUseSVCB: providers.Cannot(),
providers.CanUseTLSA: providers.Can(),
providers.CanUseDNSKEY: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot(),
providers.DocDualHost: providers.Cannot(),
providers.DocOfficiallySupported: providers.Cannot(),
}
// NewTransip creates a new TransIP provider.
func NewTransip(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
if m["AccessToken"] == "" && m["PrivateKey"] == "" {
return nil, errors.New("no TransIP AccessToken or PrivateKey provided")
}
if m["PrivateKey"] != "" && m["AccountName"] == "" {
return nil, errors.New("no AccountName given, required for authenticating with PrivateKey")
}
client, err := gotransip.NewClient(gotransip.ClientConfiguration{
Token: m["AccessToken"],
AccountName: m["AccountName"],
PrivateKeyReader: strings.NewReader(m["PrivateKey"]),
})
if err != nil {
return nil, fmt.Errorf("TransIP client fail %s", err.Error())
}
api := &transipProvider{}
api.client = &client
api.domains = &domain.Repository{Client: client}
return api, nil
}
func init() {
const providerName = "TRANSIP"
const providerMaintainer = "@blackshadev"
fns := providers.DspFuncs{
Initializer: NewTransip,
RecordAuditor: AuditRecords,
}
providers.RegisterDomainServiceProviderType(providerName, fns, features)
providers.RegisterMaintainer(providerName, providerMaintainer)
}
func (n *transipProvider) ListZones() ([]string, error) {
var domains []string
retry:
domainsMap, err := n.domains.GetAll()
if retryNeeded(err) {
goto retry
}
if err != nil {
return nil, err
}
for _, domainname := range domainsMap {
domains = append(domains, domainname.Name)
}
sort.Strings(domains)
return domains, nil
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
func (n *transipProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, curRecords models.Records) ([]*models.Correction, int, error) {
removeDomainNameserversFromDomainRecords(dc)
result, err := diff2.ByZone(curRecords, dc, nil)
if err != nil {
return nil, 0, err
}
if !result.HasChanges {
return []*models.Correction{}, result.ActualChangeCount, nil
}
msg := fmt.Sprintf("Zone update for %s\n%s", dc.Name, strings.Join(result.Msgs, "\n"))
corrections := []*models.Correction{
{
Msg: msg,
F: func() error {
nativeDNSEntries, err := recordsToNative(result.DesiredPlus)
if err != nil {
return err
}
retry:
err = n.domains.ReplaceDNSEntries(dc.Name, nativeDNSEntries)
if retryNeeded(err) {
goto retry
}
return err
},
},
}
return corrections, result.ActualChangeCount, err
}
// GetZoneRecords returns all records within given zone
func (n *transipProvider) GetZoneRecords(domainName string, meta map[string]string) (models.Records, error) {
retry:
entries, err := n.domains.GetDNSEntries(domainName)
if retryNeeded(err) {
goto retry
}
if err != nil {
return nil, err
}
existingRecords := []*models.RecordConfig{}
for _, entry := range entries {
rts, err := nativeToRecord(entry, domainName)
if err != nil {
return nil, err
}
existingRecords = append(existingRecords, rts)
}
return existingRecords, nil
}
// GetNameservers returns the nameservers of the given zone
func (n *transipProvider) GetNameservers(domainName string) ([]*models.Nameserver, error) {
var nss []string
retry:
entries, err := n.domains.GetNameservers(domainName)
if retryNeeded(err) {
goto retry
}
if err != nil {
return nil, err
}
for _, entry := range entries {
nss = append(nss, entry.Hostname)
}
return models.ToNameservers(nss)
}
func recordsToNative(records models.Records) ([]domain.DNSEntry, error) {
entries := make([]domain.DNSEntry, len(records))
for iX, record := range records {
entry, err := recordToNative(record)
if err != nil {
return nil, err
}
entries[iX] = entry
}
return entries, nil
}
func recordToNative(config *models.RecordConfig) (domain.DNSEntry, error) {
return domain.DNSEntry{
Name: config.Name,
Expire: int(config.TTL),
Type: config.Type,
Content: config.GetTargetCombinedFunc(nil),
}, nil
}
func nativeToRecord(entry domain.DNSEntry, origin string) (*models.RecordConfig, error) {
rc := &models.RecordConfig{
TTL: uint32(entry.Expire),
Type: entry.Type,
Original: entry,
}
rc.SetLabel(entry.Name, origin)
if err := rc.PopulateFromStringFunc(entry.Type, entry.Content, origin, nil); err != nil {
return nil, fmt.Errorf("unparsable record received from TransIP: %w", err)
}
return rc, nil
}
// removeDomainNameserversFromDomainRecords removes the nameserver records from the dc.Records which are already defined as the Domain nameservers
func removeDomainNameserversFromDomainRecords(dc *models.DomainConfig) {
nameserverLookup := map[string]interface{}{}
for _, nameserver := range dc.Nameservers {
nameserverLookup[nameserver.Name] = nil
}
newList := make([]*models.RecordConfig, 0, len(dc.Records))
for _, rec := range dc.Records {
dotLessNameFQDN := strings.TrimRight(rec.GetTargetField(), ".")
_, recordInDCNameservers := nameserverLookup[dotLessNameFQDN]
if rec.Type == "NS" && recordInDCNameservers {
continue
}
newList = append(newList, rec)
}
dc.Records = newList
}