mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-07 10:06: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.
375 lines
10 KiB
Go
375 lines
10 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/kubernetes-incubator/external-dns/endpoint"
|
|
"github.com/kubernetes-incubator/external-dns/plan"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/transip/gotransip"
|
|
transip "github.com/transip/gotransip/domain"
|
|
)
|
|
|
|
const (
|
|
// 60 seconds is the current minimal TTL for TransIP and will replace unconfigured
|
|
// TTL's for Endpoints
|
|
transipMinimalValidTTL = 60
|
|
)
|
|
|
|
// TransIPProvider is an implementation of Provider for TransIP.
|
|
type TransIPProvider struct {
|
|
client gotransip.SOAPClient
|
|
domainFilter DomainFilter
|
|
dryRun bool
|
|
}
|
|
|
|
// NewTransIPProvider initializes a new TransIP Provider.
|
|
func NewTransIPProvider(accountName, privateKeyFile string, domainFilter DomainFilter, dryRun bool) (*TransIPProvider, error) {
|
|
// check given arguments
|
|
if accountName == "" {
|
|
return nil, errors.New("required --transip-account not set")
|
|
}
|
|
|
|
if privateKeyFile == "" {
|
|
return nil, errors.New("required --transip-keyfile not set")
|
|
}
|
|
|
|
var apiMode gotransip.APIMode
|
|
if dryRun {
|
|
apiMode = gotransip.APIModeReadOnly
|
|
} else {
|
|
apiMode = gotransip.APIModeReadWrite
|
|
}
|
|
|
|
// create new TransIP API client
|
|
c, err := gotransip.NewSOAPClient(gotransip.ClientConfig{
|
|
AccountName: accountName,
|
|
PrivateKeyPath: privateKeyFile,
|
|
Mode: apiMode,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not setup TransIP API client: %s", err.Error())
|
|
}
|
|
|
|
// return tipCloud struct
|
|
return &TransIPProvider{
|
|
client: c,
|
|
domainFilter: domainFilter,
|
|
dryRun: dryRun,
|
|
}, nil
|
|
}
|
|
|
|
// ApplyChanges applies a given set of changes in a given zone.
|
|
func (p *TransIPProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
|
// build zonefinder with all our zones so we can use FindZone
|
|
// and a mapping of zones and their domain name
|
|
zones, err := p.fetchZones()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
zoneNameMapper := zoneIDName{}
|
|
zonesByName := make(map[string]transip.Domain)
|
|
updatedZones := make(map[string]bool)
|
|
for _, zone := range zones {
|
|
// TransIP API doesn't expose a unique identifier for zones, other than than
|
|
// the domain name itself
|
|
zoneNameMapper.Add(zone.Name, zone.Name)
|
|
zonesByName[zone.Name] = zone
|
|
}
|
|
|
|
// first see if we need to delete anything
|
|
for _, ep := range changes.Delete {
|
|
log.WithFields(log.Fields{"record": ep.DNSName, "type": ep.RecordType}).Info("endpoint has to go")
|
|
|
|
zone, err := p.zoneForZoneName(ep.DNSName, zoneNameMapper, zonesByName)
|
|
if err != nil {
|
|
log.Errorf("could not find zone for %s: %s", ep.DNSName, err.Error())
|
|
continue
|
|
}
|
|
|
|
log.Debugf("removing records for %s", zone.Name)
|
|
|
|
// remove current records from DNS entry set
|
|
entries := p.removeEndpointFromEntries(ep, zone)
|
|
|
|
// update zone in zone map
|
|
zone.DNSEntries = entries
|
|
zonesByName[zone.Name] = zone
|
|
// flag zone for updating
|
|
updatedZones[zone.Name] = true
|
|
}
|
|
|
|
for _, ep := range changes.Create {
|
|
log.WithFields(log.Fields{"record": ep.DNSName, "type": ep.RecordType}).Info("endpoint is missing")
|
|
|
|
zone, err := p.zoneForZoneName(ep.DNSName, zoneNameMapper, zonesByName)
|
|
if err != nil {
|
|
log.Errorf("could not find zone for %s: %s", ep.DNSName, err.Error())
|
|
continue
|
|
}
|
|
|
|
log.Debugf("creating records for %s", zone.Name)
|
|
|
|
// add new entries to set
|
|
zone.DNSEntries = p.addEndpointToEntries(ep, zone, zone.DNSEntries)
|
|
|
|
// update zone in zone map
|
|
zonesByName[zone.Name] = zone
|
|
// flag zone for updating
|
|
updatedZones[zone.Name] = true
|
|
log.WithFields(log.Fields{"zone": zone.Name}).Debug("flagging for update")
|
|
}
|
|
|
|
for _, ep := range changes.UpdateNew {
|
|
log.WithFields(log.Fields{"record": ep.DNSName, "type": ep.RecordType}).Debug("needs updating")
|
|
|
|
zone, err := p.zoneForZoneName(ep.DNSName, zoneNameMapper, zonesByName)
|
|
if err != nil {
|
|
log.WithFields(log.Fields{"record": ep.DNSName}).Warn(err.Error())
|
|
continue
|
|
}
|
|
|
|
// updating the records is basically finding all matching records according
|
|
// to the name and the type, removing them from the set and add the new
|
|
// records
|
|
log.WithFields(log.Fields{
|
|
"zone": zone.Name,
|
|
"dnsname": ep.DNSName,
|
|
"recordtype": ep.RecordType,
|
|
}).Debug("removing matching entries")
|
|
|
|
// remove current records from DNS entry set
|
|
entries := p.removeEndpointFromEntries(ep, zone)
|
|
|
|
// add new entries to set
|
|
entries = p.addEndpointToEntries(ep, zone, entries)
|
|
|
|
// check to see if actually anything changed in the DNSEntry set
|
|
if p.dnsEntriesAreEqual(entries, zone.DNSEntries) {
|
|
log.WithFields(log.Fields{"zone": zone.Name}).Debug("not updating identical entries")
|
|
continue
|
|
}
|
|
|
|
// update zone in zone map
|
|
zone.DNSEntries = entries
|
|
zonesByName[zone.Name] = zone
|
|
// flag zone for updating
|
|
updatedZones[zone.Name] = true
|
|
|
|
log.WithFields(log.Fields{"zone": zone.Name}).Debug("flagging for update")
|
|
}
|
|
|
|
// go over all updated zones and set new DNSEntry set
|
|
for uz := range updatedZones {
|
|
zone, ok := zonesByName[uz]
|
|
if !ok {
|
|
log.WithFields(log.Fields{"zone": uz}).Debug("updated zone no longer found")
|
|
continue
|
|
}
|
|
|
|
if p.dryRun {
|
|
log.WithFields(log.Fields{"zone": zone.Name}).Info("not updating in dry-run mode")
|
|
continue
|
|
}
|
|
|
|
log.WithFields(log.Fields{"zone": zone.Name}).Info("updating DNS entries")
|
|
if err := transip.SetDNSEntries(p.client, zone.Name, zone.DNSEntries); err != nil {
|
|
log.WithFields(log.Fields{"zone": zone.Name, "error": err.Error()}).Warn("failed to update")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// fetchZones returns a list of all domains within the account
|
|
func (p *TransIPProvider) fetchZones() ([]transip.Domain, error) {
|
|
domainNames, err := transip.GetDomainNames(p.client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
domains, err := transip.BatchGetInfo(p.client, domainNames)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var zones []transip.Domain
|
|
for _, d := range domains {
|
|
if !p.domainFilter.Match(d.Name) {
|
|
continue
|
|
}
|
|
|
|
zones = append(zones, d)
|
|
}
|
|
|
|
return zones, nil
|
|
}
|
|
|
|
// Zones returns the list of hosted zones.
|
|
func (p *TransIPProvider) Zones() ([]transip.Domain, error) {
|
|
zones, err := p.fetchZones()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return zones, nil
|
|
}
|
|
|
|
// Records returns the list of records in a given zone.
|
|
func (p *TransIPProvider) Records() ([]*endpoint.Endpoint, error) {
|
|
zones, err := p.Zones()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var endpoints []*endpoint.Endpoint
|
|
var name string
|
|
// go over all zones and their DNS entries and create endpoints for them
|
|
for _, zone := range zones {
|
|
for _, r := range zone.DNSEntries {
|
|
if !supportedRecordType(string(r.Type)) {
|
|
continue
|
|
}
|
|
|
|
name = p.endpointNameForRecord(r, zone)
|
|
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, string(r.Type), endpoint.TTL(r.TTL), r.Content))
|
|
}
|
|
}
|
|
|
|
return endpoints, nil
|
|
}
|
|
|
|
// endpointNameForRecord returns "www.example.org" for DNSEntry with Name "www" and
|
|
// Doman with Name "example.org"
|
|
func (p *TransIPProvider) endpointNameForRecord(r transip.DNSEntry, d transip.Domain) string {
|
|
// root name is identified by "@" and should be translated to domain name for
|
|
// the endpoint entry.
|
|
if r.Name == "@" {
|
|
return d.Name
|
|
}
|
|
|
|
return fmt.Sprintf("%s.%s", r.Name, d.Name)
|
|
}
|
|
|
|
// recordNameForEndpoint returns "www" for Endpoint with DNSName "www.example.org"
|
|
// and Domain with Name "example.org"
|
|
func (p *TransIPProvider) recordNameForEndpoint(ep *endpoint.Endpoint, d transip.Domain) string {
|
|
// root name is identified by "@" and should be translated to domain name for
|
|
// the endpoint entry.
|
|
if ep.DNSName == d.Name {
|
|
return "@"
|
|
}
|
|
|
|
return strings.TrimSuffix(ep.DNSName, "."+d.Name)
|
|
}
|
|
|
|
// getMinimalValidTTL returns max between given Endpoint's RecordTTL and
|
|
// transipMinimalValidTTL
|
|
func (p *TransIPProvider) getMinimalValidTTL(ep *endpoint.Endpoint) int64 {
|
|
// TTL cannot be lower than transipMinimalValidTTL
|
|
if ep.RecordTTL < transipMinimalValidTTL {
|
|
return transipMinimalValidTTL
|
|
}
|
|
|
|
return int64(ep.RecordTTL)
|
|
}
|
|
|
|
// dnsEntriesAreEqual compares the entries in 2 sets and returns true if the
|
|
// content of the entries is equal
|
|
func (p *TransIPProvider) dnsEntriesAreEqual(a, b transip.DNSEntries) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
|
|
match := 0
|
|
for _, aa := range a {
|
|
for _, bb := range b {
|
|
if aa.Content != bb.Content {
|
|
continue
|
|
}
|
|
|
|
if aa.Name != bb.Name {
|
|
continue
|
|
}
|
|
|
|
if aa.TTL != bb.TTL {
|
|
continue
|
|
}
|
|
|
|
if aa.Type != bb.Type {
|
|
continue
|
|
}
|
|
|
|
match += 1
|
|
}
|
|
}
|
|
|
|
return (len(a) == match)
|
|
}
|
|
|
|
// removeEndpointFromEntries removes DNS entries from zone's set that match the
|
|
// type and name from given endpoint and returns the resulting DNS entry set
|
|
func (p *TransIPProvider) removeEndpointFromEntries(ep *endpoint.Endpoint, zone transip.Domain) transip.DNSEntries {
|
|
// create new entry set
|
|
entries := transip.DNSEntries{}
|
|
// go over each DNS entry to see if it is a match
|
|
for _, e := range zone.DNSEntries {
|
|
// if we have match, don't copy it to the new entry set
|
|
if p.endpointNameForRecord(e, zone) == ep.DNSName && string(e.Type) == ep.RecordType {
|
|
log.WithFields(log.Fields{
|
|
"name": e.Name,
|
|
"content": e.Content,
|
|
"type": e.Type,
|
|
}).Debug("found match")
|
|
continue
|
|
}
|
|
|
|
entries = append(entries, e)
|
|
}
|
|
|
|
return entries
|
|
}
|
|
|
|
// addEndpointToEntries creates DNS entries for given endpoint and returns
|
|
// resulting DNS entry set
|
|
func (p *TransIPProvider) addEndpointToEntries(ep *endpoint.Endpoint, zone transip.Domain, entries transip.DNSEntries) transip.DNSEntries {
|
|
ttl := p.getMinimalValidTTL(ep)
|
|
for _, target := range ep.Targets {
|
|
log.WithFields(log.Fields{
|
|
"zone": zone.Name,
|
|
"dnsname": ep.DNSName,
|
|
"recordtype": ep.RecordType,
|
|
"ttl": ttl,
|
|
"target": target,
|
|
}).Debugf("adding new record")
|
|
entries = append(entries, transip.DNSEntry{
|
|
Name: p.recordNameForEndpoint(ep, zone),
|
|
TTL: ttl,
|
|
Type: transip.DNSEntryType(ep.RecordType),
|
|
Content: target,
|
|
})
|
|
}
|
|
|
|
return entries
|
|
}
|
|
|
|
// zoneForZoneName returns the zone mapped to given name or error if zone could
|
|
// not be found
|
|
func (p *TransIPProvider) zoneForZoneName(name string, m zoneIDName, z map[string]transip.Domain) (transip.Domain, error) {
|
|
_, zoneName := m.FindZone(name)
|
|
if zoneName == "" {
|
|
return transip.Domain{}, fmt.Errorf("could not find zoneName for %s", name)
|
|
}
|
|
|
|
zone, ok := z[zoneName]
|
|
if !ok {
|
|
return zone, fmt.Errorf("could not find zone for %s", zoneName)
|
|
}
|
|
|
|
return zone, nil
|
|
}
|