sigs.k8s.io/external-dns@v0.14.1/provider/ns1/ns1.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package ns1
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"fmt"
    23  	"net/http"
    24  	"os"
    25  	"strings"
    26  
    27  	log "github.com/sirupsen/logrus"
    28  	api "gopkg.in/ns1/ns1-go.v2/rest"
    29  	"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
    30  
    31  	"sigs.k8s.io/external-dns/endpoint"
    32  	"sigs.k8s.io/external-dns/plan"
    33  	"sigs.k8s.io/external-dns/provider"
    34  )
    35  
    36  const (
    37  	// ns1Create is a ChangeAction enum value
    38  	ns1Create = "CREATE"
    39  	// ns1Delete is a ChangeAction enum value
    40  	ns1Delete = "DELETE"
    41  	// ns1Update is a ChangeAction enum value
    42  	ns1Update = "UPDATE"
    43  	// ns1DefaultTTL is the default ttl for ttls that are not set
    44  	ns1DefaultTTL = 10
    45  )
    46  
    47  // NS1DomainClient is a subset of the NS1 API the the provider uses, to ease testing
    48  type NS1DomainClient interface {
    49  	CreateRecord(r *dns.Record) (*http.Response, error)
    50  	DeleteRecord(zone string, domain string, t string) (*http.Response, error)
    51  	UpdateRecord(r *dns.Record) (*http.Response, error)
    52  	GetZone(zone string) (*dns.Zone, *http.Response, error)
    53  	ListZones() ([]*dns.Zone, *http.Response, error)
    54  }
    55  
    56  // NS1DomainService wraps the API and fulfills the NS1DomainClient interface
    57  type NS1DomainService struct {
    58  	service *api.Client
    59  }
    60  
    61  // CreateRecord wraps the Create method of the API's Record service
    62  func (n NS1DomainService) CreateRecord(r *dns.Record) (*http.Response, error) {
    63  	return n.service.Records.Create(r)
    64  }
    65  
    66  // DeleteRecord wraps the Delete method of the API's Record service
    67  func (n NS1DomainService) DeleteRecord(zone string, domain string, t string) (*http.Response, error) {
    68  	return n.service.Records.Delete(zone, domain, t)
    69  }
    70  
    71  // UpdateRecord wraps the Update method of the API's Record service
    72  func (n NS1DomainService) UpdateRecord(r *dns.Record) (*http.Response, error) {
    73  	return n.service.Records.Update(r)
    74  }
    75  
    76  // GetZone wraps the Get method of the API's Zones service
    77  func (n NS1DomainService) GetZone(zone string) (*dns.Zone, *http.Response, error) {
    78  	return n.service.Zones.Get(zone, true)
    79  }
    80  
    81  // ListZones wraps the List method of the API's Zones service
    82  func (n NS1DomainService) ListZones() ([]*dns.Zone, *http.Response, error) {
    83  	return n.service.Zones.List()
    84  }
    85  
    86  // NS1Config passes cli args to the NS1Provider
    87  type NS1Config struct {
    88  	DomainFilter  endpoint.DomainFilter
    89  	ZoneIDFilter  provider.ZoneIDFilter
    90  	NS1Endpoint   string
    91  	NS1IgnoreSSL  bool
    92  	DryRun        bool
    93  	MinTTLSeconds int
    94  }
    95  
    96  // NS1Provider is the NS1 provider
    97  type NS1Provider struct {
    98  	provider.BaseProvider
    99  	client        NS1DomainClient
   100  	domainFilter  endpoint.DomainFilter
   101  	zoneIDFilter  provider.ZoneIDFilter
   102  	dryRun        bool
   103  	minTTLSeconds int
   104  }
   105  
   106  // NewNS1Provider creates a new NS1 Provider
   107  func NewNS1Provider(config NS1Config) (*NS1Provider, error) {
   108  	return newNS1ProviderWithHTTPClient(config, http.DefaultClient)
   109  }
   110  
   111  func newNS1ProviderWithHTTPClient(config NS1Config, client *http.Client) (*NS1Provider, error) {
   112  	token, ok := os.LookupEnv("NS1_APIKEY")
   113  	if !ok {
   114  		return nil, fmt.Errorf("NS1_APIKEY environment variable is not set")
   115  	}
   116  	clientArgs := []func(*api.Client){api.SetAPIKey(token)}
   117  	if config.NS1Endpoint != "" {
   118  		log.Infof("ns1-endpoint flag is set, targeting endpoint at %s", config.NS1Endpoint)
   119  		clientArgs = append(clientArgs, api.SetEndpoint(config.NS1Endpoint))
   120  	}
   121  
   122  	if config.NS1IgnoreSSL {
   123  		log.Info("ns1-ignoressl flag is True, skipping SSL verification")
   124  		defaultTransport := http.DefaultTransport.(*http.Transport)
   125  		tr := &http.Transport{
   126  			Proxy:                 defaultTransport.Proxy,
   127  			DialContext:           defaultTransport.DialContext,
   128  			MaxIdleConns:          defaultTransport.MaxIdleConns,
   129  			IdleConnTimeout:       defaultTransport.IdleConnTimeout,
   130  			ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
   131  			TLSHandshakeTimeout:   defaultTransport.TLSHandshakeTimeout,
   132  			TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
   133  		}
   134  		client.Transport = tr
   135  	}
   136  
   137  	apiClient := api.NewClient(client, clientArgs...)
   138  
   139  	provider := &NS1Provider{
   140  		client:        NS1DomainService{apiClient},
   141  		domainFilter:  config.DomainFilter,
   142  		zoneIDFilter:  config.ZoneIDFilter,
   143  		minTTLSeconds: config.MinTTLSeconds,
   144  	}
   145  	return provider, nil
   146  }
   147  
   148  // Records returns the endpoints this provider knows about
   149  func (p *NS1Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
   150  	zones, err := p.zonesFiltered()
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	var endpoints []*endpoint.Endpoint
   156  
   157  	for _, zone := range zones {
   158  		// TODO handle Header Codes
   159  		zoneData, _, err := p.client.GetZone(zone.String())
   160  		if err != nil {
   161  			return nil, err
   162  		}
   163  
   164  		for _, record := range zoneData.Records {
   165  			if provider.SupportedRecordType(record.Type) {
   166  				endpoints = append(endpoints, endpoint.NewEndpointWithTTL(
   167  					record.Domain,
   168  					record.Type,
   169  					endpoint.TTL(record.TTL),
   170  					record.ShortAns...,
   171  				),
   172  				)
   173  			}
   174  		}
   175  	}
   176  
   177  	return endpoints, nil
   178  }
   179  
   180  // ns1BuildRecord returns a dns.Record for a change set
   181  func (p *NS1Provider) ns1BuildRecord(zoneName string, change *ns1Change) *dns.Record {
   182  	record := dns.NewRecord(zoneName, change.Endpoint.DNSName, change.Endpoint.RecordType, map[string]string{}, []string{})
   183  	for _, v := range change.Endpoint.Targets {
   184  		record.AddAnswer(dns.NewAnswer(strings.Split(v, " ")))
   185  	}
   186  	// set default ttl, but respect minTTLSeconds
   187  	ttl := ns1DefaultTTL
   188  	if p.minTTLSeconds > ttl {
   189  		ttl = p.minTTLSeconds
   190  	}
   191  	if change.Endpoint.RecordTTL.IsConfigured() {
   192  		ttl = int(change.Endpoint.RecordTTL)
   193  	}
   194  	record.TTL = ttl
   195  
   196  	return record
   197  }
   198  
   199  // ns1SubmitChanges takes an array of changes and sends them to NS1
   200  func (p *NS1Provider) ns1SubmitChanges(changes []*ns1Change) error {
   201  	// return early if there is nothing to change
   202  	if len(changes) == 0 {
   203  		return nil
   204  	}
   205  
   206  	zones, err := p.zonesFiltered()
   207  	if err != nil {
   208  		return err
   209  	}
   210  
   211  	// separate into per-zone change sets to be passed to the API.
   212  	changesByZone := ns1ChangesByZone(zones, changes)
   213  	for zoneName, changes := range changesByZone {
   214  		for _, change := range changes {
   215  			record := p.ns1BuildRecord(zoneName, change)
   216  			logFields := log.Fields{
   217  				"record": record.Domain,
   218  				"type":   record.Type,
   219  				"ttl":    record.TTL,
   220  				"action": change.Action,
   221  				"zone":   zoneName,
   222  			}
   223  
   224  			log.WithFields(logFields).Info("Changing record.")
   225  
   226  			if p.dryRun {
   227  				continue
   228  			}
   229  
   230  			switch change.Action {
   231  			case ns1Create:
   232  				_, err := p.client.CreateRecord(record)
   233  				if err != nil {
   234  					return err
   235  				}
   236  			case ns1Delete:
   237  				_, err := p.client.DeleteRecord(zoneName, record.Domain, record.Type)
   238  				if err != nil {
   239  					return err
   240  				}
   241  			case ns1Update:
   242  				_, err := p.client.UpdateRecord(record)
   243  				if err != nil {
   244  					return err
   245  				}
   246  			}
   247  		}
   248  	}
   249  	return nil
   250  }
   251  
   252  // Zones returns the list of hosted zones.
   253  func (p *NS1Provider) zonesFiltered() ([]*dns.Zone, error) {
   254  	// TODO handle Header Codes
   255  	zones, _, err := p.client.ListZones()
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	toReturn := []*dns.Zone{}
   261  
   262  	for _, z := range zones {
   263  		if p.domainFilter.Match(z.Zone) && p.zoneIDFilter.Match(z.ID) {
   264  			toReturn = append(toReturn, z)
   265  			log.Debugf("Matched %s", z.Zone)
   266  		} else {
   267  			log.Debugf("Filtered %s", z.Zone)
   268  		}
   269  	}
   270  
   271  	return toReturn, nil
   272  }
   273  
   274  // ns1Change differentiates between ChangeActions
   275  type ns1Change struct {
   276  	Action   string
   277  	Endpoint *endpoint.Endpoint
   278  }
   279  
   280  // ApplyChanges applies a given set of changes in a given zone.
   281  func (p *NS1Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   282  	combinedChanges := make([]*ns1Change, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
   283  
   284  	combinedChanges = append(combinedChanges, newNS1Changes(ns1Create, changes.Create)...)
   285  	combinedChanges = append(combinedChanges, newNS1Changes(ns1Update, changes.UpdateNew)...)
   286  	combinedChanges = append(combinedChanges, newNS1Changes(ns1Delete, changes.Delete)...)
   287  
   288  	return p.ns1SubmitChanges(combinedChanges)
   289  }
   290  
   291  // newNS1Changes returns a collection of Changes based on the given records and action.
   292  func newNS1Changes(action string, endpoints []*endpoint.Endpoint) []*ns1Change {
   293  	changes := make([]*ns1Change, 0, len(endpoints))
   294  
   295  	for _, endpoint := range endpoints {
   296  		changes = append(changes, &ns1Change{
   297  			Action:   action,
   298  			Endpoint: endpoint,
   299  		},
   300  		)
   301  	}
   302  
   303  	return changes
   304  }
   305  
   306  // ns1ChangesByZone separates a multi-zone change into a single change per zone.
   307  func ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*ns1Change {
   308  	changes := make(map[string][]*ns1Change)
   309  	zoneNameIDMapper := provider.ZoneIDName{}
   310  	for _, z := range zones {
   311  		zoneNameIDMapper.Add(z.Zone, z.Zone)
   312  		changes[z.Zone] = []*ns1Change{}
   313  	}
   314  
   315  	for _, c := range changeSets {
   316  		zone, _ := zoneNameIDMapper.FindZone(c.Endpoint.DNSName)
   317  		if zone == "" {
   318  			log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.Endpoint.DNSName)
   319  			continue
   320  		}
   321  		changes[zone] = append(changes[zone], c)
   322  	}
   323  
   324  	return changes
   325  }