external-dns/provider/ns1/ns1.go
Raffaele Di Fazio db4224c705 restructures the providers (build only)
Signed-off-by: Raffaele Di Fazio <difazio.raffaele@gmail.com>
2020-05-08 13:05:17 +02:00

320 lines
9.0 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 ns1
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"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
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 endpoint.DomainFilter
ZoneIDFilter provider.ZoneIDFilter
NS1Endpoint string
NS1IgnoreSSL bool
DryRun bool
}
// NS1Provider is the NS1 provider
type NS1Provider struct {
client NS1DomainClient
domainFilter endpoint.DomainFilter
zoneIDFilter provider.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 {
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(ctx context.Context) ([]*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 provider.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 := provider.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
}