external-dns/provider/rcode0.go
2020-01-06 08:08:35 -08:00

340 lines
7.9 KiB
Go

/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
import (
"context"
"fmt"
"net/url"
"os"
"strings"
rc0 "github.com/nic-at/rc0go"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
)
// RcodeZeroProvider implements the DNS provider for RcodeZero Anycast DNS.
type RcodeZeroProvider struct {
Client *rc0.Client
DomainFilter DomainFilter
DryRun bool
TXTEncrypt bool
Key []byte
}
// NewRcodeZeroProvider creates a new RcodeZero Anycast DNS provider.
//
// Returns the provider or an error if a provider could not be created.
func NewRcodeZeroProvider(domainFilter DomainFilter, dryRun bool, txtEnc bool) (*RcodeZeroProvider, error) {
client, err := rc0.NewClient(os.Getenv("RC0_API_KEY"))
if err != nil {
return nil, err
}
value := os.Getenv("RC0_BASE_URL")
if len(value) != 0 {
client.BaseURL, err = url.Parse(os.Getenv("RC0_BASE_URL"))
}
if err != nil {
return nil, fmt.Errorf("failed to initialize rcodezero provider: %v", err)
}
provider := &RcodeZeroProvider{
Client: client,
DomainFilter: domainFilter,
DryRun: dryRun,
TXTEncrypt: txtEnc,
}
if txtEnc {
provider.Key = []byte(os.Getenv("RC0_ENC_KEY"))
}
return provider, nil
}
// Zones returns filtered zones if filter is set
func (p *RcodeZeroProvider) Zones() ([]*rc0.Zone, error) {
var result []*rc0.Zone
zones, err := p.fetchZones()
if err != nil {
return nil, err
}
for _, zone := range zones {
if p.DomainFilter.Match(zone.Domain) {
result = append(result, zone)
}
}
return result, nil
}
// Records returns resource records
//
// Decrypts TXT records if TXT-Encrypt flag is set and key is provided
func (p *RcodeZeroProvider) Records() ([]*endpoint.Endpoint, error) {
zones, err := p.Zones()
if err != nil {
return nil, err
}
var endpoints []*endpoint.Endpoint
for _, zone := range zones {
rrset, err := p.fetchRecords(zone.Domain)
if err != nil {
return nil, err
}
for _, r := range rrset {
if supportedRecordType(r.Type) {
if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(r.Type, "TXT") {
p.Client.RRSet.DecryptTXT(p.Key, r)
}
if len(r.Records) > 1 {
for _, _r := range r.Records {
if !_r.Disabled {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), _r.Content))
}
}
} else {
if !r.Records[0].Disabled {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), r.Records[0].Content))
}
}
}
}
}
return endpoints, nil
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *RcodeZeroProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
combinedChanges := make([]*rc0.RRSetChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeADD, changes.Create)...)
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeUPDATE, changes.UpdateNew)...)
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeDELETE, changes.Delete)...)
return p.submitChanges(combinedChanges)
}
// Helper function
func rcodezeroChangesByZone(zones []*rc0.Zone, changeSet []*rc0.RRSetChange) map[string][]*rc0.RRSetChange {
changes := make(map[string][]*rc0.RRSetChange)
zoneNameIDMapper := zoneIDName{}
for _, z := range zones {
zoneNameIDMapper.Add(z.Domain, z.Domain)
changes[z.Domain] = []*rc0.RRSetChange{}
}
for _, c := range changeSet {
zone, _ := zoneNameIDMapper.FindZone(c.Name)
if zone == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.Name)
continue
}
changes[zone] = append(changes[zone], c)
}
return changes
}
// Helper function
func (p *RcodeZeroProvider) fetchRecords(zoneName string) ([]*rc0.RRType, error) {
var allRecords []*rc0.RRType
listOptions := rc0.NewListOptions()
for {
records, page, err := p.Client.RRSet.List(zoneName, listOptions)
if err != nil {
return nil, err
}
allRecords = append(allRecords, records...)
if page == nil || (page.CurrentPage == page.LastPage) {
break
}
listOptions.SetPageNumber(page.CurrentPage + 1)
}
return allRecords, nil
}
// Helper function
func (p *RcodeZeroProvider) fetchZones() ([]*rc0.Zone, error) {
var allZones []*rc0.Zone
listOptions := rc0.NewListOptions()
for {
zones, page, err := p.Client.Zones.List(listOptions)
if err != nil {
return nil, err
}
allZones = append(allZones, zones...)
if page == nil || page.IsLastPage() {
break
}
listOptions.SetPageNumber(page.CurrentPage + 1)
}
return allZones, nil
}
// Helper function to submit changes.
//
// Changes are submitted by change type.
func (p *RcodeZeroProvider) submitChanges(changes []*rc0.RRSetChange) error {
if len(changes) == 0 {
return nil
}
zones, err := p.Zones()
if err != nil {
return err
}
// separate into per-zone change sets to be passed to the API.
changesByZone := rcodezeroChangesByZone(zones, changes)
for zoneName, changes := range changesByZone {
for _, change := range changes {
logFields := log.Fields{
"record": change.Name,
"content": change.Records[0].Content,
"type": change.Type,
"action": change.ChangeType,
"zone": zoneName,
}
log.WithFields(logFields).Info("Changing record.")
if p.DryRun {
continue
}
// to avoid accidentally adding extra dot if already present
change.Name = strings.TrimSuffix(change.Name, ".") + "."
switch change.ChangeType {
case rc0.ChangeTypeADD:
sr, err := p.Client.RRSet.Create(zoneName, []*rc0.RRSetChange{change})
if err != nil {
return err
}
if sr.HasError() {
return fmt.Errorf("adding new RR resulted in an error: %v", sr.Message)
}
case rc0.ChangeTypeUPDATE:
sr, err := p.Client.RRSet.Edit(zoneName, []*rc0.RRSetChange{change})
if err != nil {
return err
}
if sr.HasError() {
return fmt.Errorf("updating existing RR resulted in an error: %v", sr.Message)
}
case rc0.ChangeTypeDELETE:
sr, err := p.Client.RRSet.Delete(zoneName, []*rc0.RRSetChange{change})
if err != nil {
return err
}
if sr.HasError() {
return fmt.Errorf("deleting existing RR resulted in an error: %v", sr.Message)
}
default:
return fmt.Errorf("unsupported changeType submitted: %v", change.ChangeType)
}
}
}
return nil
}
// NewRcodezeroChanges returns a RcodeZero specific array with rrset change objects.
func (p *RcodeZeroProvider) NewRcodezeroChanges(action string, endpoints []*endpoint.Endpoint) []*rc0.RRSetChange {
changes := make([]*rc0.RRSetChange, 0, len(endpoints))
for _, _endpoint := range endpoints {
changes = append(changes, p.NewRcodezeroChange(action, _endpoint))
}
return changes
}
// NewRcodezeroChange returns a RcodeZero specific rrset change object.
func (p *RcodeZeroProvider) NewRcodezeroChange(action string, endpoint *endpoint.Endpoint) *rc0.RRSetChange {
change := &rc0.RRSetChange{
Type: endpoint.RecordType,
ChangeType: action,
Name: endpoint.DNSName,
Records: []*rc0.Record{{
Disabled: false,
Content: endpoint.Targets[0],
}},
}
if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(endpoint.RecordType, "TXT") {
p.Client.RRSet.EncryptTXT(p.Key, change)
}
return change
}