sigs.k8s.io/external-dns@v0.14.1/provider/vinyldns/vinyldns.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 vinyldns
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"strings"
    24  
    25  	log "github.com/sirupsen/logrus"
    26  	"github.com/vinyldns/go-vinyldns/vinyldns"
    27  
    28  	"sigs.k8s.io/external-dns/endpoint"
    29  	"sigs.k8s.io/external-dns/plan"
    30  	"sigs.k8s.io/external-dns/provider"
    31  )
    32  
    33  const (
    34  	vinyldnsCreate = "CREATE"
    35  	vinyldnsDelete = "DELETE"
    36  	vinyldnsUpdate = "UPDATE"
    37  
    38  	vinyldnsRecordTTL = 300
    39  )
    40  
    41  type vinyldnsZoneInterface interface {
    42  	Zones() ([]vinyldns.Zone, error)
    43  	RecordSets(id string) ([]vinyldns.RecordSet, error)
    44  	RecordSet(zoneID, recordSetID string) (vinyldns.RecordSet, error)
    45  	RecordSetCreate(rs *vinyldns.RecordSet) (*vinyldns.RecordSetUpdateResponse, error)
    46  	RecordSetUpdate(rs *vinyldns.RecordSet) (*vinyldns.RecordSetUpdateResponse, error)
    47  	RecordSetDelete(zoneID, recordSetID string) (*vinyldns.RecordSetUpdateResponse, error)
    48  }
    49  
    50  type vinyldnsProvider struct {
    51  	provider.BaseProvider
    52  	client       vinyldnsZoneInterface
    53  	zoneFilter   provider.ZoneIDFilter
    54  	domainFilter endpoint.DomainFilter
    55  	dryRun       bool
    56  }
    57  
    58  type vinyldnsChange struct {
    59  	Action            string
    60  	ResourceRecordSet vinyldns.RecordSet
    61  }
    62  
    63  // NewVinylDNSProvider provides support for VinylDNS records
    64  func NewVinylDNSProvider(domainFilter endpoint.DomainFilter, zoneFilter provider.ZoneIDFilter, dryRun bool) (provider.Provider, error) {
    65  	_, ok := os.LookupEnv("VINYLDNS_ACCESS_KEY")
    66  	if !ok {
    67  		return nil, fmt.Errorf("no vinyldns access key found")
    68  	}
    69  
    70  	client := vinyldns.NewClientFromEnv()
    71  
    72  	return &vinyldnsProvider{
    73  		client:       client,
    74  		dryRun:       dryRun,
    75  		zoneFilter:   zoneFilter,
    76  		domainFilter: domainFilter,
    77  	}, nil
    78  }
    79  
    80  func (p *vinyldnsProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
    81  	zones, err := p.client.Zones()
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  
    86  	for _, zone := range zones {
    87  		if !p.zoneFilter.Match(zone.ID) {
    88  			continue
    89  		}
    90  
    91  		if !p.domainFilter.Match(zone.Name) {
    92  			continue
    93  		}
    94  
    95  		log.Infof(fmt.Sprintf("Zone: [%s:%s]", zone.ID, zone.Name))
    96  		records, err := p.client.RecordSets(zone.ID)
    97  		if err != nil {
    98  			return nil, err
    99  		}
   100  
   101  		for _, r := range records {
   102  			if provider.SupportedRecordType(r.Type) {
   103  				recordsCount := len(r.Records)
   104  				log.Debugf(fmt.Sprintf("%s.%s.%d.%s", r.Name, r.Type, recordsCount, zone.Name))
   105  
   106  				// TODO: AAAA Records
   107  				if len(r.Records) > 0 {
   108  					targets := make([]string, len(r.Records))
   109  					for idx, rr := range r.Records {
   110  						switch r.Type {
   111  						case "A":
   112  							targets[idx] = rr.Address
   113  						case "CNAME":
   114  							targets[idx] = rr.CName
   115  						case "TXT":
   116  							targets[idx] = rr.Text
   117  						}
   118  					}
   119  
   120  					endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name+"."+zone.Name, r.Type, endpoint.TTL(r.TTL), targets...))
   121  				}
   122  			}
   123  		}
   124  	}
   125  
   126  	return endpoints, nil
   127  }
   128  
   129  func vinyldnsSuitableZone(hostname string, zones []vinyldns.Zone) *vinyldns.Zone {
   130  	var zone *vinyldns.Zone
   131  	for _, z := range zones {
   132  		log.Debugf("hostname: %s and zoneName: %s", hostname, z.Name)
   133  		// Adding a . as vinyl appends it to each zone record
   134  		if strings.HasSuffix(hostname+".", z.Name) {
   135  			zone = &z
   136  			break
   137  		}
   138  	}
   139  	return zone
   140  }
   141  
   142  func (p *vinyldnsProvider) submitChanges(changes []*vinyldnsChange) error {
   143  	if len(changes) == 0 {
   144  		log.Infof("All records are already up to date")
   145  		return nil
   146  	}
   147  
   148  	zones, err := p.client.Zones()
   149  	if err != nil {
   150  		return err
   151  	}
   152  
   153  	for _, change := range changes {
   154  		zone := vinyldnsSuitableZone(change.ResourceRecordSet.Name, zones)
   155  		if zone == nil {
   156  			log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", change.ResourceRecordSet.Name)
   157  			continue
   158  		}
   159  
   160  		change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name+".", "."+zone.Name)
   161  		change.ResourceRecordSet.ZoneID = zone.ID
   162  		log.Infof("Changing records: %s %v in zone: %s", change.Action, change.ResourceRecordSet, zone.Name)
   163  
   164  		if !p.dryRun {
   165  			switch change.Action {
   166  			case vinyldnsCreate:
   167  				_, err := p.client.RecordSetCreate(&change.ResourceRecordSet)
   168  				if err != nil {
   169  					return err
   170  				}
   171  			case vinyldnsUpdate:
   172  				recordID, err := p.findRecordSetID(zone.ID, change.ResourceRecordSet.Name)
   173  				if err != nil {
   174  					return err
   175  				}
   176  				change.ResourceRecordSet.ID = recordID
   177  				_, err = p.client.RecordSetUpdate(&change.ResourceRecordSet)
   178  				if err != nil {
   179  					return err
   180  				}
   181  			case vinyldnsDelete:
   182  				recordID, err := p.findRecordSetID(zone.ID, change.ResourceRecordSet.Name)
   183  				if err != nil {
   184  					return err
   185  				}
   186  				_, err = p.client.RecordSetDelete(zone.ID, recordID)
   187  				if err != nil {
   188  					return err
   189  				}
   190  			}
   191  		}
   192  	}
   193  
   194  	return nil
   195  }
   196  
   197  func (p *vinyldnsProvider) findRecordSetID(zoneID string, recordSetName string) (recordID string, err error) {
   198  	records, err := p.client.RecordSets(zoneID)
   199  	if err != nil {
   200  		return "", err
   201  	}
   202  
   203  	for _, r := range records {
   204  		if r.Name == recordSetName {
   205  			return r.ID, nil
   206  		}
   207  	}
   208  
   209  	return "", fmt.Errorf("record not found")
   210  }
   211  
   212  func (p *vinyldnsProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   213  	combinedChanges := make([]*vinyldnsChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
   214  
   215  	combinedChanges = append(combinedChanges, newVinylDNSChanges(vinyldnsCreate, changes.Create)...)
   216  	combinedChanges = append(combinedChanges, newVinylDNSChanges(vinyldnsUpdate, changes.UpdateNew)...)
   217  	combinedChanges = append(combinedChanges, newVinylDNSChanges(vinyldnsDelete, changes.Delete)...)
   218  
   219  	return p.submitChanges(combinedChanges)
   220  }
   221  
   222  // newVinylDNSChanges returns a collection of Changes based on the given records and action.
   223  func newVinylDNSChanges(action string, endpoints []*endpoint.Endpoint) []*vinyldnsChange {
   224  	changes := make([]*vinyldnsChange, 0, len(endpoints))
   225  
   226  	for _, e := range endpoints {
   227  		changes = append(changes, newVinylDNSChange(action, e))
   228  	}
   229  
   230  	return changes
   231  }
   232  
   233  func newVinylDNSChange(action string, endpoint *endpoint.Endpoint) *vinyldnsChange {
   234  	ttl := vinyldnsRecordTTL
   235  	if endpoint.RecordTTL.IsConfigured() {
   236  		ttl = int(endpoint.RecordTTL)
   237  	}
   238  
   239  	records := []vinyldns.Record{}
   240  
   241  	// TODO: AAAA
   242  	if endpoint.RecordType == "CNAME" {
   243  		records = []vinyldns.Record{
   244  			{
   245  				CName: endpoint.Targets[0],
   246  			},
   247  		}
   248  	} else if endpoint.RecordType == "TXT" {
   249  		records = []vinyldns.Record{
   250  			{
   251  				Text: endpoint.Targets[0],
   252  			},
   253  		}
   254  	} else if endpoint.RecordType == "A" {
   255  		records = []vinyldns.Record{
   256  			{
   257  				Address: endpoint.Targets[0],
   258  			},
   259  		}
   260  	}
   261  
   262  	change := &vinyldnsChange{
   263  		Action: action,
   264  		ResourceRecordSet: vinyldns.RecordSet{
   265  			Name:    endpoint.DNSName,
   266  			Type:    endpoint.RecordType,
   267  			TTL:     ttl,
   268  			Records: records,
   269  		},
   270  	}
   271  	return change
   272  }