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.
320 lines
8.9 KiB
Go
320 lines
8.9 KiB
Go
/*
|
|
Copyright 2017 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"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
api "gopkg.in/ns1/ns1-go.v2/rest"
|
|
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
|
|
|
|
"github.com/kubernetes-incubator/external-dns/endpoint"
|
|
"github.com/kubernetes-incubator/external-dns/plan"
|
|
)
|
|
|
|
const (
|
|
// ns1Create is a ChangeAction enum value
|
|
ns1Create = "CREATE"
|
|
// ns1Delete is a ChangeAction enum value
|
|
ns1Delete = "DELETE"
|
|
// ns1Update is a ChangeAction enum value
|
|
ns1Update = "UPDATE"
|
|
// ns1DefaultTTL is the default ttl for ttls that are not set
|
|
ns1DefaultTTL = 10
|
|
)
|
|
|
|
// NS1DomainClient is a subset of the NS1 API the the provider uses, to ease testing
|
|
type NS1DomainClient interface {
|
|
CreateRecord(r *dns.Record) (*http.Response, error)
|
|
DeleteRecord(zone string, domain string, t string) (*http.Response, error)
|
|
UpdateRecord(r *dns.Record) (*http.Response, error)
|
|
GetZone(zone string) (*dns.Zone, *http.Response, error)
|
|
ListZones() ([]*dns.Zone, *http.Response, error)
|
|
}
|
|
|
|
// NS1DomainService wraps the API and fulfills the NS1DomainClient interface
|
|
type NS1DomainService struct {
|
|
service *api.Client
|
|
}
|
|
|
|
// CreateRecord wraps the Create method of the API's Record service
|
|
func (n NS1DomainService) CreateRecord(r *dns.Record) (*http.Response, error) {
|
|
return n.service.Records.Create(r)
|
|
}
|
|
|
|
// DeleteRecord wraps the Delete method of the API's Record service
|
|
func (n NS1DomainService) DeleteRecord(zone string, domain string, t string) (*http.Response, error) {
|
|
return n.service.Records.Delete(zone, domain, t)
|
|
}
|
|
|
|
// UpdateRecord wraps the Update method of the API's Record service
|
|
func (n NS1DomainService) UpdateRecord(r *dns.Record) (*http.Response, error) {
|
|
return n.service.Records.Update(r)
|
|
}
|
|
|
|
// GetZone wraps the Get method of the API's Zones service
|
|
func (n NS1DomainService) GetZone(zone string) (*dns.Zone, *http.Response, error) {
|
|
return n.service.Zones.Get(zone)
|
|
}
|
|
|
|
// ListZones wraps the List method of the API's Zones service
|
|
func (n NS1DomainService) ListZones() ([]*dns.Zone, *http.Response, error) {
|
|
return n.service.Zones.List()
|
|
}
|
|
|
|
// NS1Config passes cli args to the NS1Provider
|
|
type NS1Config struct {
|
|
DomainFilter DomainFilter
|
|
ZoneIDFilter ZoneIDFilter
|
|
NS1Endpoint string
|
|
NS1IgnoreSSL bool
|
|
DryRun bool
|
|
}
|
|
|
|
// NS1Provider is the NS1 provider
|
|
type NS1Provider struct {
|
|
client NS1DomainClient
|
|
domainFilter DomainFilter
|
|
zoneIDFilter ZoneIDFilter
|
|
dryRun bool
|
|
}
|
|
|
|
// NewNS1Provider creates a new NS1 Provider
|
|
func NewNS1Provider(config NS1Config) (*NS1Provider, error) {
|
|
return newNS1ProviderWithHTTPClient(config, http.DefaultClient)
|
|
}
|
|
|
|
func newNS1ProviderWithHTTPClient(config NS1Config, client *http.Client) (*NS1Provider, error) {
|
|
token, ok := os.LookupEnv("NS1_APIKEY")
|
|
if !ok {
|
|
return nil, fmt.Errorf("NS1_APIKEY environment variable is not set")
|
|
}
|
|
clientArgs := []func(*api.Client){api.SetAPIKey(token)}
|
|
if config.NS1Endpoint != "" {
|
|
log.Infof("ns1-endpoint flag is set, targeting endpoint at %s", config.NS1Endpoint)
|
|
clientArgs = append(clientArgs, api.SetEndpoint(config.NS1Endpoint))
|
|
}
|
|
|
|
if config.NS1IgnoreSSL == true {
|
|
log.Info("ns1-ignoressl flag is True, skipping SSL verification")
|
|
defaultTransport := http.DefaultTransport.(*http.Transport)
|
|
tr := &http.Transport{
|
|
Proxy: defaultTransport.Proxy,
|
|
DialContext: defaultTransport.DialContext,
|
|
MaxIdleConns: defaultTransport.MaxIdleConns,
|
|
IdleConnTimeout: defaultTransport.IdleConnTimeout,
|
|
ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
|
|
TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout,
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
}
|
|
client.Transport = tr
|
|
}
|
|
|
|
apiClient := api.NewClient(client, clientArgs...)
|
|
|
|
provider := &NS1Provider{
|
|
client: NS1DomainService{apiClient},
|
|
domainFilter: config.DomainFilter,
|
|
zoneIDFilter: config.ZoneIDFilter,
|
|
}
|
|
return provider, nil
|
|
}
|
|
|
|
// Records returns the endpoints this provider knows about
|
|
func (p *NS1Provider) Records() ([]*endpoint.Endpoint, error) {
|
|
zones, err := p.zonesFiltered()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var endpoints []*endpoint.Endpoint
|
|
|
|
for _, zone := range zones {
|
|
|
|
// TODO handle Header Codes
|
|
zoneData, _, err := p.client.GetZone(zone.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, record := range zoneData.Records {
|
|
if supportedRecordType(record.Type) {
|
|
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(
|
|
record.Domain,
|
|
record.Type,
|
|
endpoint.TTL(record.TTL),
|
|
record.ShortAns...,
|
|
),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
return endpoints, nil
|
|
}
|
|
|
|
// ns1BuildRecord returns a dns.Record for a change set
|
|
func ns1BuildRecord(zoneName string, change *ns1Change) *dns.Record {
|
|
record := dns.NewRecord(zoneName, change.Endpoint.DNSName, change.Endpoint.RecordType)
|
|
for _, v := range change.Endpoint.Targets {
|
|
record.AddAnswer(dns.NewAnswer(strings.Split(v, " ")))
|
|
}
|
|
// set detault ttl
|
|
var ttl = ns1DefaultTTL
|
|
if change.Endpoint.RecordTTL.IsConfigured() {
|
|
ttl = int(change.Endpoint.RecordTTL)
|
|
}
|
|
record.TTL = ttl
|
|
|
|
return record
|
|
}
|
|
|
|
// ns1SubmitChanges takes an array of changes and sends them to NS1
|
|
func (p *NS1Provider) ns1SubmitChanges(changes []*ns1Change) error {
|
|
// return early if there is nothing to change
|
|
if len(changes) == 0 {
|
|
return nil
|
|
}
|
|
|
|
zones, err := p.zonesFiltered()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// separate into per-zone change sets to be passed to the API.
|
|
changesByZone := ns1ChangesByZone(zones, changes)
|
|
for zoneName, changes := range changesByZone {
|
|
for _, change := range changes {
|
|
record := ns1BuildRecord(zoneName, change)
|
|
logFields := log.Fields{
|
|
"record": record.Domain,
|
|
"type": record.Type,
|
|
"ttl": record.TTL,
|
|
"action": change.Action,
|
|
"zone": zoneName,
|
|
}
|
|
|
|
log.WithFields(logFields).Info("Changing record.")
|
|
|
|
if p.dryRun {
|
|
continue
|
|
}
|
|
|
|
switch change.Action {
|
|
case ns1Create:
|
|
_, err := p.client.CreateRecord(record)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case ns1Delete:
|
|
_, err := p.client.DeleteRecord(zoneName, record.Domain, record.Type)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case ns1Update:
|
|
_, err := p.client.UpdateRecord(record)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Zones returns the list of hosted zones.
|
|
func (p *NS1Provider) zonesFiltered() ([]*dns.Zone, error) {
|
|
// TODO handle Header Codes
|
|
zones, _, err := p.client.ListZones()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
toReturn := []*dns.Zone{}
|
|
|
|
for _, z := range zones {
|
|
if p.domainFilter.Match(z.Zone) && p.zoneIDFilter.Match(z.ID) {
|
|
toReturn = append(toReturn, z)
|
|
log.Debugf("Matched %s", z.Zone)
|
|
} else {
|
|
log.Debugf("Filtered %s", z.Zone)
|
|
}
|
|
}
|
|
|
|
return toReturn, nil
|
|
}
|
|
|
|
// ns1Change differentiates between ChangeActions
|
|
type ns1Change struct {
|
|
Action string
|
|
Endpoint *endpoint.Endpoint
|
|
}
|
|
|
|
// ApplyChanges applies a given set of changes in a given zone.
|
|
func (p *NS1Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
|
combinedChanges := make([]*ns1Change, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
|
|
|
|
combinedChanges = append(combinedChanges, newNS1Changes(ns1Create, changes.Create)...)
|
|
combinedChanges = append(combinedChanges, newNS1Changes(ns1Update, changes.UpdateNew)...)
|
|
combinedChanges = append(combinedChanges, newNS1Changes(ns1Delete, changes.Delete)...)
|
|
|
|
return p.ns1SubmitChanges(combinedChanges)
|
|
}
|
|
|
|
// newNS1Changes returns a collection of Changes based on the given records and action.
|
|
func newNS1Changes(action string, endpoints []*endpoint.Endpoint) []*ns1Change {
|
|
changes := make([]*ns1Change, 0, len(endpoints))
|
|
|
|
for _, endpoint := range endpoints {
|
|
changes = append(changes, &ns1Change{
|
|
Action: action,
|
|
Endpoint: endpoint,
|
|
},
|
|
)
|
|
}
|
|
|
|
return changes
|
|
}
|
|
|
|
// ns1ChangesByZone separates a multi-zone change into a single change per zone.
|
|
func ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*ns1Change {
|
|
changes := make(map[string][]*ns1Change)
|
|
zoneNameIDMapper := zoneIDName{}
|
|
for _, z := range zones {
|
|
zoneNameIDMapper.Add(z.Zone, z.Zone)
|
|
changes[z.Zone] = []*ns1Change{}
|
|
}
|
|
|
|
for _, c := range changeSets {
|
|
zone, _ := zoneNameIDMapper.FindZone(c.Endpoint.DNSName)
|
|
if zone == "" {
|
|
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected ", c.Endpoint.DNSName)
|
|
continue
|
|
}
|
|
changes[zone] = append(changes[zone], c)
|
|
}
|
|
|
|
return changes
|
|
}
|