mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-07 18:16:57 +02:00
The controller will retrieve all the endpoints at the beginning of its loop. When changes need to be applied, the provider may need to query the endpoints again. Allow the provider to skip the queries if its data was cached.
339 lines
7.9 KiB
Go
339 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"
|
|
|
|
"github.com/kubernetes-incubator/external-dns/endpoint"
|
|
"github.com/kubernetes-incubator/external-dns/plan"
|
|
rc0 "github.com/nic-at/rc0go"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// 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
|
|
}
|