From e5de7b5359ee9b1ecfd1daa48027c583eb3fc2c2 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 20 Jun 2022 18:27:05 +0200 Subject: [PATCH] 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 * Rename api to dsp and make initializer function private Signed-off-by: Jan-Philipp Benecke Co-authored-by: Tom Limoncelli --- providers/powerdns/convert.go | 42 +++++ providers/powerdns/dns.go | 146 ++++++++++++++++ providers/powerdns/dnssec.go | 16 +- providers/powerdns/listzones.go | 19 +++ providers/powerdns/powerdnsProvider.go | 226 ++----------------------- 5 files changed, 233 insertions(+), 216 deletions(-) create mode 100644 providers/powerdns/convert.go create mode 100644 providers/powerdns/dns.go create mode 100644 providers/powerdns/listzones.go diff --git a/providers/powerdns/convert.go b/providers/powerdns/convert.go new file mode 100644 index 000000000..9540132ab --- /dev/null +++ b/providers/powerdns/convert.go @@ -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) + } +} diff --git a/providers/powerdns/dns.go b/providers/powerdns/dns.go new file mode 100644 index 000000000..d4bc5525d --- /dev/null +++ b/providers/powerdns/dns.go @@ -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 +} diff --git a/providers/powerdns/dnssec.go b/providers/powerdns/dnssec.go index 79aff3e1b..cd7c27c00 100644 --- a/providers/powerdns/dnssec.go +++ b/providers/powerdns/dnssec.go @@ -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 } diff --git a/providers/powerdns/listzones.go b/providers/powerdns/listzones.go new file mode 100644 index 000000000..0991dbc79 --- /dev/null +++ b/providers/powerdns/listzones.go @@ -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 +} diff --git a/providers/powerdns/powerdnsProvider.go b/providers/powerdns/powerdnsProvider.go index 742c0258f..9526ef5df 100644 --- a/providers/powerdns/powerdnsProvider.go +++ b/providers/powerdns/powerdnsProvider.go @@ -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 }