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:
Jan-Philipp Benecke 2022-06-20 18:27:05 +02:00 committed by GitHub
parent 959f721c04
commit e5de7b5359
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 233 additions and 216 deletions

View 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
View 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
}

View file

@ -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
}

View 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
}

View file

@ -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
}