diff --git a/providers/powerdns/diff.go b/providers/powerdns/diff.go new file mode 100644 index 000000000..c533ba270 --- /dev/null +++ b/providers/powerdns/diff.go @@ -0,0 +1,129 @@ +package powerdns + +import ( + "context" + "fmt" + "github.com/StackExchange/dnscontrol/v3/models" + "github.com/StackExchange/dnscontrol/v3/pkg/diff" + "github.com/StackExchange/dnscontrol/v3/pkg/diff2" + "github.com/mittwald/go-powerdns/apis/zones" + "strings" +) + +func (dsp *powerdnsProvider) getDiff1DomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) { + // create record diff by group + keysToUpdate, err := (diff.New(dc)).ChangedGroups(existing) + 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 := canonical(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 + var corrections []*models.Correction + corrections = append(corrections, dCorrections...) + corrections = append(corrections, cuCorrections...) + + return corrections, nil +} + +func (dsp *powerdnsProvider) getDiff2DomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) { + changes, err := diff2.ByRecordSet(existing, dc, nil) + if err != nil { + return nil, err + } + + var corrections []*models.Correction + + for _, change := range changes { + labelName := canonical(change.Key.NameFQDN) + labelType := change.Key.Type + + switch change.Type { + case diff2.REPORT: + corrections = append(corrections, &models.Correction{Msg: change.MsgsJoined}) + case diff2.CREATE, diff2.CHANGE: + labelTTL := int(change.New[0].TTL) + records := buildRecordList(change) + + corrections = append(corrections, &models.Correction{ + Msg: change.MsgsJoined, + F: func() error { + return dsp.client.Zones().AddRecordSetToZone(context.Background(), dsp.ServerName, dc.Name, zones.ResourceRecordSet{ + Name: labelName, + Type: labelType, + TTL: labelTTL, + Records: records, + ChangeType: zones.ChangeTypeReplace, + }) + }, + }) + case diff2.DELETE: + corrections = append(corrections, &models.Correction{ + Msg: change.MsgsJoined, + F: func() error { + return dsp.client.Zones().RemoveRecordSetFromZone(context.Background(), dsp.ServerName, dc.Name, labelName, labelType) + }, + }) + default: + panic(fmt.Sprintf("unhandled change.Type %s", change.Type)) + } + } + + return corrections, nil +} + +// buildRecordList returns a list of records for the PowerDNS resource record set from a change +func buildRecordList(change diff2.Change) (records []zones.Record) { + for _, recordContent := range change.New { + records = append(records, zones.Record{ + Content: recordContent.GetTargetCombined(), + }) + } + return +} + +func canonical(fqdn string) string { + return fqdn + "." +} diff --git a/providers/powerdns/dns.go b/providers/powerdns/dns.go index 116c99e13..546c92f5a 100644 --- a/providers/powerdns/dns.go +++ b/providers/powerdns/dns.go @@ -2,14 +2,11 @@ package powerdns import ( "context" - "net/http" - "strings" - "github.com/StackExchange/dnscontrol/v3/models" - "github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff2" "github.com/mittwald/go-powerdns/apis/zones" "github.com/mittwald/go-powerdns/pdnshttp" + "net/http" ) // GetNameservers returns the nameservers for a domain. @@ -49,9 +46,8 @@ func (dsp *powerdnsProvider) GetZoneRecords(domain string) (models.Records, erro // GetDomainCorrections returns a list of corrections to update a domain. func (dsp *powerdnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { - // get current zone records - curRecords, err := dsp.GetZoneRecords(dc.Name) + existing, err := dsp.GetZoneRecords(dc.Name) if err != nil { return nil, err } @@ -60,80 +56,30 @@ func (dsp *powerdnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m if err := dc.Punycode(); err != nil { return nil, err } - models.PostProcessRecords(curRecords) + models.PostProcessRecords(existing) - // create record diff by group - var keysToUpdate map[models.RecordKey][]string + var corrections []*models.Correction if !diff2.EnableDiff2 { - keysToUpdate, err = (diff.New(dc)).ChangedGroups(curRecords) + corrections, err = dsp.getDiff1DomainCorrections(dc, existing) } else { - keysToUpdate, err = (diff.NewCompat(dc)).ChangedGroups(curRecords) + corrections, err = dsp.getDiff2DomainCorrections(dc, existing) } 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 - var corrections []*models.Correction - 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 + return append(corrections, dnssecCorrections...), nil } // EnsureZoneExists creates a zone if it does not exist func (dsp *powerdnsProvider) EnsureZoneExists(domain string) error { - if _, err := dsp.client.Zones().GetZone(context.Background(), dsp.ServerName, domain+"."); err != nil { + if _, err := dsp.client.Zones().GetZone(context.Background(), dsp.ServerName, canonical(domain)); err != nil { if e, ok := err.(pdnshttp.ErrUnexpectedStatus); ok { if e.StatusCode != http.StatusNotFound { return err @@ -144,7 +90,7 @@ func (dsp *powerdnsProvider) EnsureZoneExists(domain string) error { } _, err := dsp.client.Zones().CreateZone(context.Background(), dsp.ServerName, zones.Zone{ - Name: domain + ".", + Name: canonical(domain), Type: zones.ZoneTypeZone, DNSSec: dsp.DNSSecOnCreate, Nameservers: dsp.DefaultNS, diff --git a/providers/powerdns/dnssec.go b/providers/powerdns/dnssec.go index cd7c27c00..cfeeff8a7 100644 --- a/providers/powerdns/dnssec.go +++ b/providers/powerdns/dnssec.go @@ -33,7 +33,9 @@ func (dsp *powerdnsProvider) getDNSSECCorrections(dc *models.DomainConfig) ([]*m return []*models.Correction{ { Msg: "Disable DNSSEC", - F: func() error { _, err := dsp.removeDnssec(dc.Name, keyID); return err }, + F: func() error { + return dsp.client.Cryptokeys().DeleteCryptokey(context.Background(), dsp.ServerName, dc.Name, keyID) + }, }, }, nil } @@ -43,33 +45,17 @@ func (dsp *powerdnsProvider) getDNSSECCorrections(dc *models.DomainConfig) ([]*m return []*models.Correction{ { Msg: "Enable DNSSEC", - F: func() error { _, err := dsp.enableDnssec(dc.Name); return err }, + F: func() (err error) { + _, err = dsp.client.Cryptokeys().CreateCryptokey(context.Background(), dsp.ServerName, dc.Name, cryptokeys.Cryptokey{ + KeyType: "csk", + Active: true, + Published: true, + }) + return + }, }, }, nil } return nil, nil } - -// enableDnssec creates a active and published cryptokey on this domain -func (dsp *powerdnsProvider) enableDnssec(domain string) (bool, error) { - // if there is now key, create one and enable it - _, err := dsp.client.Cryptokeys().CreateCryptokey(context.Background(), dsp.ServerName, domain, cryptokeys.Cryptokey{ - KeyType: "csk", - Active: true, - Published: true, - }) - if err != nil { - return false, err - } - return true, nil -} - -// removeDnssec removes the cryptokey from this zone -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 - } - return true, nil -} diff --git a/providers/powerdns/powerdnsProvider.go b/providers/powerdns/powerdnsProvider.go index 1598dbbb3..fbaf7738f 100644 --- a/providers/powerdns/powerdnsProvider.go +++ b/providers/powerdns/powerdnsProvider.go @@ -15,7 +15,7 @@ var features = providers.DocumentationNotes{ providers.CanUseAlias: providers.Can("Needs to be enabled in PowerDNS first", "https://doc.powerdns.com/authoritative/guides/alias.html"), providers.CanUseCAA: providers.Can(), providers.CanUseDS: providers.Can(), - providers.CanUseLOC: providers.Unimplemented(), + providers.CanUseLOC: providers.Unimplemented("Normalization within the PowerDNS API seems to be buggy, so disabled", "https://github.com/PowerDNS/pdns/issues/10558"), providers.CanUseNAPTR: providers.Can(), providers.CanUsePTR: providers.Can(), providers.CanUseSRV: providers.Can(),