Initial Skeleton From NS1 Provider

This needs unit tests and a full integration test.

Pushing this as an initial checkpoint.
This commit is contained in:
mburtless 2018-10-17 21:55:52 -04:00
parent a0976df0d9
commit bff09c20c9
3 changed files with 250 additions and 0 deletions

View File

@ -201,6 +201,14 @@ func main() {
}
case "rfc2136":
p, err = provider.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, nil)
case "ns1":
p, err = provider.NewNS1Provider(
provider.NS1Config{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
DryRun: cfg.DryRun,
},
)
default:
log.Fatalf("unknown dns provider: %s", cfg.Provider)
}

241
provider/ns1.go Normal file
View File

@ -0,0 +1,241 @@
package provider
import (
"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
)
// NS1Config passes cli args to the NS1Provider
type NS1Config struct {
DomainFilter DomainFilter
ZoneIDFilter ZoneIDFilter
DryRun bool
}
// NS1Provider is the NS1 provider
type NS1Provider struct {
client *api.Client
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")
}
apiClient := api.NewClient(client, api.SetAPIKey(token))
provider := &NS1Provider{
client: apiClient,
domainFilter: config.DomainFilter,
zoneIDFilter: config.ZoneIDFilter,
}
return provider, nil
}
func (p *NS1Provider) matchEither(id string) bool {
return p.domainFilter.Match(id) || p.zoneIDFilter.Match(id)
}
// 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.Zones.Get(zone.String())
if err != nil {
return nil, err
}
for _, record := range zoneData.Records {
if supportedRecordType(record.Type) {
name := fmt.Sprintf("%s.%s", record.Domain, zoneData.Zone)
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(
name,
record.Type,
endpoint.TTL(record.TTL),
record.ShortAns...,
),
)
}
}
}
return endpoints, nil
}
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
}
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.Records.Create(record)
if err != nil {
return err
}
case ns1Delete:
_, err := p.client.Records.Delete(zoneName, record.Domain, record.Type)
if err != nil {
return err
}
case ns1Update:
_, err := p.client.Records.Update(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.Zones.List()
if err != nil {
return nil, err
}
toReturn := []*dns.Zone{}
for _, z := range zones {
if !p.matchEither(z.Zone) && !p.matchEither(z.ID) {
continue
}
toReturn = append(toReturn, z)
}
return toReturn, nil
}
// ns1Change differentiates between ChangActions
type ns1Change struct {
Action string
Endpoint *endpoint.Endpoint
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *NS1Provider) ApplyChanges(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
}

1
provider/ns1_test.go Normal file
View File

@ -0,0 +1 @@
package provider