mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-09-10 15:14:25 +08:00
MAINT: Restructuring of the PowerDNS DSP based on the layout of CSCGlobal (#1549)
* Restructure PowerDNS DSP based on layout for CSCGlobal Signed-off-by: Jan-Philipp Benecke <jan-philipp@bnck.me> * Rename api to dsp and make initializer function private Signed-off-by: Jan-Philipp Benecke <jan-philipp@bnck.me> Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
parent
959f721c04
commit
e5de7b5359
5 changed files with 233 additions and 216 deletions
42
providers/powerdns/convert.go
Normal file
42
providers/powerdns/convert.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package powerdns
|
||||
|
||||
import (
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
"github.com/mittwald/go-powerdns/apis/zones"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// toRecordConfig converts a PowerDNS DNSRecord to a RecordConfig. #rtype_variations
|
||||
func toRecordConfig(domain string, r zones.Record, ttl int, name string, rtype string) (*models.RecordConfig, error) {
|
||||
// trimming trailing dot and domain from name
|
||||
name = strings.TrimSuffix(name, domain+".")
|
||||
name = strings.TrimSuffix(name, ".")
|
||||
|
||||
rc := &models.RecordConfig{
|
||||
TTL: uint32(ttl),
|
||||
Original: r,
|
||||
Type: rtype,
|
||||
}
|
||||
rc.SetLabel(name, domain)
|
||||
|
||||
content := r.Content
|
||||
switch rtype {
|
||||
case "ALIAS":
|
||||
return rc, rc.SetTarget(r.Content)
|
||||
case "CNAME", "NS":
|
||||
return rc, rc.SetTarget(dnsutil.AddOrigin(content, domain))
|
||||
case "CAA":
|
||||
return rc, rc.SetTargetCAAString(content)
|
||||
case "DS":
|
||||
return rc, rc.SetTargetDSString(content)
|
||||
case "MX":
|
||||
return rc, rc.SetTargetMXString(content)
|
||||
case "SRV":
|
||||
return rc, rc.SetTargetSRVString(content)
|
||||
case "NAPTR":
|
||||
return rc, rc.SetTargetNAPTRString(content)
|
||||
default:
|
||||
return rc, rc.PopulateFromString(rtype, content, domain)
|
||||
}
|
||||
}
|
146
providers/powerdns/dns.go
Normal file
146
providers/powerdns/dns.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package powerdns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
||||
"github.com/mittwald/go-powerdns/apis/zones"
|
||||
"github.com/mittwald/go-powerdns/pdnshttp"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetNameservers returns the nameservers for a domain.
|
||||
func (dsp *powerdnsProvider) GetNameservers(string) ([]*models.Nameserver, error) {
|
||||
var r []string
|
||||
for _, j := range dsp.nameservers {
|
||||
r = append(r, j.Name)
|
||||
}
|
||||
return models.ToNameservers(r)
|
||||
}
|
||||
|
||||
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||
func (dsp *powerdnsProvider) GetZoneRecords(domain string) (models.Records, error) {
|
||||
zone, err := dsp.client.Zones().GetZone(context.Background(), dsp.ServerName, domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
curRecords := models.Records{}
|
||||
// loop over grouped records by type, called RRSet
|
||||
for _, rrset := range zone.ResourceRecordSets {
|
||||
if rrset.Type == "SOA" {
|
||||
continue
|
||||
}
|
||||
// loop over single records of this group and create records
|
||||
for _, pdnsRecord := range rrset.Records {
|
||||
r, err := toRecordConfig(domain, pdnsRecord, rrset.TTL, rrset.Name, rrset.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
curRecords = append(curRecords, r)
|
||||
}
|
||||
}
|
||||
|
||||
return curRecords, nil
|
||||
}
|
||||
|
||||
// GetDomainCorrections returns a list of corrections to update a domain.
|
||||
func (dsp *powerdnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
var corrections []*models.Correction
|
||||
|
||||
// get current zone records
|
||||
curRecords, err := dsp.GetZoneRecords(dc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// post-process records
|
||||
if err := dc.Punycode(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
models.PostProcessRecords(curRecords)
|
||||
|
||||
// create record diff by group
|
||||
keysToUpdate, err := (diff.New(dc)).ChangedGroups(curRecords)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
desiredRecords := dc.Records.GroupedByKey()
|
||||
|
||||
var cuCorrections []*models.Correction
|
||||
var dCorrections []*models.Correction
|
||||
|
||||
// add create/update and delete corrections separately
|
||||
for label, msgs := range keysToUpdate {
|
||||
labelName := label.NameFQDN + "."
|
||||
labelType := label.Type
|
||||
msgJoined := strings.Join(msgs, "\n ")
|
||||
|
||||
if _, ok := desiredRecords[label]; !ok {
|
||||
// no record found so delete it
|
||||
dCorrections = append(dCorrections, &models.Correction{
|
||||
Msg: msgJoined,
|
||||
F: func() error {
|
||||
return dsp.client.Zones().RemoveRecordSetFromZone(context.Background(), dsp.ServerName, dc.Name, labelName, labelType)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// record found so create or update it
|
||||
ttl := desiredRecords[label][0].TTL
|
||||
var records []zones.Record
|
||||
for _, recordContent := range desiredRecords[label] {
|
||||
records = append(records, zones.Record{
|
||||
Content: recordContent.GetTargetCombined(),
|
||||
})
|
||||
}
|
||||
cuCorrections = append(cuCorrections, &models.Correction{
|
||||
Msg: msgJoined,
|
||||
F: func() error {
|
||||
return dsp.client.Zones().AddRecordSetToZone(context.Background(), dsp.ServerName, dc.Name, zones.ResourceRecordSet{
|
||||
Name: labelName,
|
||||
Type: labelType,
|
||||
TTL: int(ttl),
|
||||
Records: records,
|
||||
ChangeType: zones.ChangeTypeReplace,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// append corrections in the right order
|
||||
// delete corrections must be run first to avoid correlations with existing RR
|
||||
corrections = append(corrections, dCorrections...)
|
||||
corrections = append(corrections, cuCorrections...)
|
||||
|
||||
// DNSSec corrections
|
||||
dnssecCorrections, err := dsp.getDNSSECCorrections(dc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
corrections = append(corrections, dnssecCorrections...)
|
||||
|
||||
return corrections, nil
|
||||
}
|
||||
|
||||
// EnsureDomainExists adds a domain to the DNS service if it does not exist
|
||||
func (dsp *powerdnsProvider) EnsureDomainExists(domain string) error {
|
||||
if _, err := dsp.client.Zones().GetZone(context.Background(), dsp.ServerName, domain+"."); err != nil {
|
||||
if e, ok := err.(pdnshttp.ErrUnexpectedStatus); ok {
|
||||
if e.StatusCode != http.StatusNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else { // domain seems to be there
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := dsp.client.Zones().CreateZone(context.Background(), dsp.ServerName, zones.Zone{
|
||||
Name: domain + ".",
|
||||
Type: zones.ZoneTypeZone,
|
||||
DNSSec: dsp.DNSSecOnCreate,
|
||||
Nameservers: dsp.DefaultNS,
|
||||
})
|
||||
return err
|
||||
}
|
|
@ -8,8 +8,8 @@ import (
|
|||
)
|
||||
|
||||
// getDNSSECCorrections returns corrections that update a domain's DNSSEC state.
|
||||
func (api *powerdnsProvider) getDNSSECCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
zoneCryptokeys, getErr := api.client.Cryptokeys().ListCryptokeys(context.Background(), api.ServerName, dc.Name)
|
||||
func (dsp *powerdnsProvider) getDNSSECCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
zoneCryptokeys, getErr := dsp.client.Cryptokeys().ListCryptokeys(context.Background(), dsp.ServerName, dc.Name)
|
||||
if getErr != nil {
|
||||
return nil, getErr
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ func (api *powerdnsProvider) getDNSSECCorrections(dc *models.DomainConfig) ([]*m
|
|||
return []*models.Correction{
|
||||
{
|
||||
Msg: "Disable DNSSEC",
|
||||
F: func() error { _, err := api.removeDnssec(dc.Name, keyID); return err },
|
||||
F: func() error { _, err := dsp.removeDnssec(dc.Name, keyID); return err },
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ func (api *powerdnsProvider) getDNSSECCorrections(dc *models.DomainConfig) ([]*m
|
|||
return []*models.Correction{
|
||||
{
|
||||
Msg: "Enable DNSSEC",
|
||||
F: func() error { _, err := api.enableDnssec(dc.Name); return err },
|
||||
F: func() error { _, err := dsp.enableDnssec(dc.Name); return err },
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
@ -52,9 +52,9 @@ func (api *powerdnsProvider) getDNSSECCorrections(dc *models.DomainConfig) ([]*m
|
|||
}
|
||||
|
||||
// enableDnssec creates a active and published cryptokey on this domain
|
||||
func (api *powerdnsProvider) enableDnssec(domain string) (bool, error) {
|
||||
func (dsp *powerdnsProvider) enableDnssec(domain string) (bool, error) {
|
||||
// if there is now key, create one and enable it
|
||||
_, err := api.client.Cryptokeys().CreateCryptokey(context.Background(), api.ServerName, domain, cryptokeys.Cryptokey{
|
||||
_, err := dsp.client.Cryptokeys().CreateCryptokey(context.Background(), dsp.ServerName, domain, cryptokeys.Cryptokey{
|
||||
KeyType: "csk",
|
||||
Active: true,
|
||||
Published: true,
|
||||
|
@ -66,8 +66,8 @@ func (api *powerdnsProvider) enableDnssec(domain string) (bool, error) {
|
|||
}
|
||||
|
||||
// removeDnssec removes the cryptokey from this zone
|
||||
func (api *powerdnsProvider) removeDnssec(domain string, keyID int) (bool, error) {
|
||||
err := api.client.Cryptokeys().DeleteCryptokey(context.Background(), api.ServerName, domain, keyID)
|
||||
func (dsp *powerdnsProvider) removeDnssec(domain string, keyID int) (bool, error) {
|
||||
err := dsp.client.Cryptokeys().DeleteCryptokey(context.Background(), dsp.ServerName, domain, keyID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
19
providers/powerdns/listzones.go
Normal file
19
providers/powerdns/listzones.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package powerdns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ListZones returns all the zones in an account
|
||||
func (dsp *powerdnsProvider) ListZones() ([]string, error) {
|
||||
var result []string
|
||||
myZones, err := dsp.client.Zones().ListZones(context.Background(), dsp.ServerName)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
for _, zone := range myZones {
|
||||
result = append(result, strings.TrimSuffix(zone.Name, "."))
|
||||
}
|
||||
return result, nil
|
||||
}
|
|
@ -1,19 +1,11 @@
|
|||
package powerdns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
||||
"github.com/StackExchange/dnscontrol/v3/providers"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
pdns "github.com/mittwald/go-powerdns"
|
||||
"github.com/mittwald/go-powerdns/apis/zones"
|
||||
"github.com/mittwald/go-powerdns/pdnshttp"
|
||||
)
|
||||
|
||||
var features = providers.DocumentationNotes{
|
||||
|
@ -34,7 +26,7 @@ var features = providers.DocumentationNotes{
|
|||
|
||||
func init() {
|
||||
fns := providers.DspFuncs{
|
||||
Initializer: NewProvider,
|
||||
Initializer: newDSP,
|
||||
RecordAuditor: AuditRecords,
|
||||
}
|
||||
providers.RegisterDomainServiceProviderType("POWERDNS", fns, features)
|
||||
|
@ -52,228 +44,46 @@ type powerdnsProvider struct {
|
|||
nameservers []*models.Nameserver
|
||||
}
|
||||
|
||||
// NewProvider initializes a PowerDNS DNSServiceProvider.
|
||||
func NewProvider(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||
api := &powerdnsProvider{}
|
||||
// newDSP initializes a PowerDNS DNSServiceProvider.
|
||||
func newDSP(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||
dsp := &powerdnsProvider{}
|
||||
|
||||
api.APIKey = m["apiKey"]
|
||||
if api.APIKey == "" {
|
||||
dsp.APIKey = m["apiKey"]
|
||||
if dsp.APIKey == "" {
|
||||
return nil, fmt.Errorf("PowerDNS API Key is required")
|
||||
}
|
||||
|
||||
api.APIUrl = m["apiUrl"]
|
||||
if api.APIUrl == "" {
|
||||
dsp.APIUrl = m["apiUrl"]
|
||||
if dsp.APIUrl == "" {
|
||||
return nil, fmt.Errorf("PowerDNS API URL is required")
|
||||
}
|
||||
|
||||
api.ServerName = m["serverName"]
|
||||
if api.ServerName == "" {
|
||||
dsp.ServerName = m["serverName"]
|
||||
if dsp.ServerName == "" {
|
||||
return nil, fmt.Errorf("PowerDNS server name is required")
|
||||
}
|
||||
|
||||
// load js config
|
||||
if len(metadata) != 0 {
|
||||
err := json.Unmarshal(metadata, api)
|
||||
err := json.Unmarshal(metadata, dsp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var nss []string
|
||||
for _, ns := range api.DefaultNS {
|
||||
for _, ns := range dsp.DefaultNS {
|
||||
nss = append(nss, ns[0:len(ns)-1])
|
||||
}
|
||||
var err error
|
||||
api.nameservers, err = models.ToNameservers(nss)
|
||||
dsp.nameservers, err = models.ToNameservers(nss)
|
||||
if err != nil {
|
||||
return api, err
|
||||
return dsp, err
|
||||
}
|
||||
|
||||
var clientErr error
|
||||
api.client, clientErr = pdns.New(
|
||||
pdns.WithBaseURL(api.APIUrl),
|
||||
pdns.WithAPIKeyAuthentication(api.APIKey),
|
||||
dsp.client, clientErr = pdns.New(
|
||||
pdns.WithBaseURL(dsp.APIUrl),
|
||||
pdns.WithAPIKeyAuthentication(dsp.APIKey),
|
||||
)
|
||||
return api, clientErr
|
||||
}
|
||||
|
||||
// GetNameservers returns the nameservers for a domain.
|
||||
func (api *powerdnsProvider) GetNameservers(string) ([]*models.Nameserver, error) {
|
||||
var r []string
|
||||
for _, j := range api.nameservers {
|
||||
r = append(r, j.Name)
|
||||
}
|
||||
return models.ToNameservers(r)
|
||||
}
|
||||
|
||||
// ListZones returns all the zones in an account
|
||||
func (api *powerdnsProvider) ListZones() ([]string, error) {
|
||||
var result []string
|
||||
myZones, err := api.client.Zones().ListZones(context.Background(), api.ServerName)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
for _, zone := range myZones {
|
||||
result = append(result, strings.TrimSuffix(zone.Name, "."))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||
func (api *powerdnsProvider) GetZoneRecords(domain string) (models.Records, error) {
|
||||
zone, err := api.client.Zones().GetZone(context.Background(), api.ServerName, domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
curRecords := models.Records{}
|
||||
// loop over grouped records by type, called RRSet
|
||||
for _, rrset := range zone.ResourceRecordSets {
|
||||
if rrset.Type == "SOA" {
|
||||
continue
|
||||
}
|
||||
// loop over single records of this group and create records
|
||||
for _, pdnsRecord := range rrset.Records {
|
||||
r, err := toRecordConfig(domain, pdnsRecord, rrset.TTL, rrset.Name, rrset.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
curRecords = append(curRecords, r)
|
||||
}
|
||||
}
|
||||
|
||||
return curRecords, nil
|
||||
}
|
||||
|
||||
// GetDomainCorrections returns a list of corrections to update a domain.
|
||||
func (api *powerdnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
var corrections []*models.Correction
|
||||
|
||||
// get current zone records
|
||||
curRecords, err := api.GetZoneRecords(dc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// post-process records
|
||||
if err := dc.Punycode(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
models.PostProcessRecords(curRecords)
|
||||
|
||||
// create record diff by group
|
||||
keysToUpdate, err := (diff.New(dc)).ChangedGroups(curRecords)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
desiredRecords := dc.Records.GroupedByKey()
|
||||
|
||||
var cuCorrections []*models.Correction
|
||||
var dCorrections []*models.Correction
|
||||
|
||||
// add create/update and delete corrections separately
|
||||
for label, msgs := range keysToUpdate {
|
||||
labelName := label.NameFQDN + "."
|
||||
labelType := label.Type
|
||||
msgJoined := strings.Join(msgs, "\n ")
|
||||
|
||||
if _, ok := desiredRecords[label]; !ok {
|
||||
// no record found so delete it
|
||||
dCorrections = append(dCorrections, &models.Correction{
|
||||
Msg: msgJoined,
|
||||
F: func() error {
|
||||
return api.client.Zones().RemoveRecordSetFromZone(context.Background(), api.ServerName, dc.Name, labelName, labelType)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// record found so create or update it
|
||||
ttl := desiredRecords[label][0].TTL
|
||||
var records []zones.Record
|
||||
for _, recordContent := range desiredRecords[label] {
|
||||
records = append(records, zones.Record{
|
||||
Content: recordContent.GetTargetCombined(),
|
||||
})
|
||||
}
|
||||
cuCorrections = append(cuCorrections, &models.Correction{
|
||||
Msg: msgJoined,
|
||||
F: func() error {
|
||||
return api.client.Zones().AddRecordSetToZone(context.Background(), api.ServerName, dc.Name, zones.ResourceRecordSet{
|
||||
Name: labelName,
|
||||
Type: labelType,
|
||||
TTL: int(ttl),
|
||||
Records: records,
|
||||
ChangeType: zones.ChangeTypeReplace,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// append corrections in the right order
|
||||
// delete corrections must be run first to avoid correlations with existing RR
|
||||
corrections = append(corrections, dCorrections...)
|
||||
corrections = append(corrections, cuCorrections...)
|
||||
|
||||
// DNSSec corrections
|
||||
dnssecCorrections, err := api.getDNSSECCorrections(dc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
corrections = append(corrections, dnssecCorrections...)
|
||||
|
||||
return corrections, nil
|
||||
}
|
||||
|
||||
// EnsureDomainExists adds a domain to the DNS service if it does not exist
|
||||
func (api *powerdnsProvider) EnsureDomainExists(domain string) error {
|
||||
if _, err := api.client.Zones().GetZone(context.Background(), api.ServerName, domain+"."); err != nil {
|
||||
if e, ok := err.(pdnshttp.ErrUnexpectedStatus); ok {
|
||||
if e.StatusCode != http.StatusNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else { // domain seems to be there
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := api.client.Zones().CreateZone(context.Background(), api.ServerName, zones.Zone{
|
||||
Name: domain + ".",
|
||||
Type: zones.ZoneTypeZone,
|
||||
DNSSec: api.DNSSecOnCreate,
|
||||
Nameservers: api.DefaultNS,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// toRecordConfig converts a PowerDNS DNSRecord to a RecordConfig. #rtype_variations
|
||||
func toRecordConfig(domain string, r zones.Record, ttl int, name string, rtype string) (*models.RecordConfig, error) {
|
||||
// trimming trailing dot and domain from name
|
||||
name = strings.TrimSuffix(name, domain+".")
|
||||
name = strings.TrimSuffix(name, ".")
|
||||
|
||||
rc := &models.RecordConfig{
|
||||
TTL: uint32(ttl),
|
||||
Original: r,
|
||||
Type: rtype,
|
||||
}
|
||||
rc.SetLabel(name, domain)
|
||||
|
||||
content := r.Content
|
||||
switch rtype {
|
||||
case "ALIAS":
|
||||
return rc, rc.SetTarget(r.Content)
|
||||
case "CNAME", "NS":
|
||||
return rc, rc.SetTarget(dnsutil.AddOrigin(content, domain))
|
||||
case "CAA":
|
||||
return rc, rc.SetTargetCAAString(content)
|
||||
case "DS":
|
||||
return rc, rc.SetTargetDSString(content)
|
||||
case "MX":
|
||||
return rc, rc.SetTargetMXString(content)
|
||||
case "SRV":
|
||||
return rc, rc.SetTargetSRVString(content)
|
||||
case "NAPTR":
|
||||
return rc, rc.SetTargetNAPTRString(content)
|
||||
default:
|
||||
return rc, rc.PopulateFromString(rtype, content, domain)
|
||||
}
|
||||
return dsp, clientErr
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue