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

     1  /*
     2  Copyright 2020 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 godaddy
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"strings"
    25  
    26  	log "github.com/sirupsen/logrus"
    27  	"golang.org/x/sync/errgroup"
    28  
    29  	"sigs.k8s.io/external-dns/endpoint"
    30  	"sigs.k8s.io/external-dns/plan"
    31  	"sigs.k8s.io/external-dns/provider"
    32  )
    33  
    34  const (
    35  	gdMinimalTTL = 600
    36  	gdCreate     = 0
    37  	gdReplace    = 1
    38  	gdDelete     = 2
    39  )
    40  
    41  var actionNames = []string{
    42  	"create",
    43  	"replace",
    44  	"delete",
    45  }
    46  
    47  const domainsURI = "/v1/domains?statuses=ACTIVE,PENDING_DNS_ACTIVE"
    48  
    49  // ErrRecordToMutateNotFound when ApplyChange has to update/delete and didn't found the record in the existing zone (Change with no record ID)
    50  var ErrRecordToMutateNotFound = errors.New("record to mutate not found in current zone")
    51  
    52  type gdClient interface {
    53  	Patch(string, interface{}, interface{}) error
    54  	Post(string, interface{}, interface{}) error
    55  	Put(string, interface{}, interface{}) error
    56  	Get(string, interface{}) error
    57  	Delete(string, interface{}) error
    58  }
    59  
    60  // GDProvider declare GoDaddy provider
    61  type GDProvider struct {
    62  	provider.BaseProvider
    63  
    64  	domainFilter endpoint.DomainFilter
    65  	client       gdClient
    66  	ttl          int64
    67  	DryRun       bool
    68  }
    69  
    70  type gdEndpoint struct {
    71  	endpoint *endpoint.Endpoint
    72  	action   int
    73  }
    74  
    75  type gdRecordField struct {
    76  	Data     string  `json:"data"`
    77  	Name     string  `json:"name"`
    78  	TTL      int64   `json:"ttl"`
    79  	Type     string  `json:"type"`
    80  	Port     *int    `json:"port,omitempty"`
    81  	Priority *int    `json:"priority,omitempty"`
    82  	Weight   *int64  `json:"weight,omitempty"`
    83  	Protocol *string `json:"protocol,omitempty"`
    84  	Service  *string `json:"service,omitempty"`
    85  }
    86  
    87  type gdReplaceRecordField struct {
    88  	Data     string  `json:"data"`
    89  	TTL      int64   `json:"ttl"`
    90  	Port     *int    `json:"port,omitempty"`
    91  	Priority *int    `json:"priority,omitempty"`
    92  	Weight   *int64  `json:"weight,omitempty"`
    93  	Protocol *string `json:"protocol,omitempty"`
    94  	Service  *string `json:"service,omitempty"`
    95  }
    96  
    97  type gdRecords struct {
    98  	records []gdRecordField
    99  	changed bool
   100  	zone    string
   101  }
   102  
   103  type gdZone struct {
   104  	CreatedAt           string
   105  	Domain              string
   106  	DomainID            int64
   107  	ExpirationProtected bool
   108  	Expires             string
   109  	ExposeWhois         bool
   110  	HoldRegistrar       bool
   111  	Locked              bool
   112  	NameServers         *[]string
   113  	Privacy             bool
   114  	RenewAuto           bool
   115  	RenewDeadline       string
   116  	Renewable           bool
   117  	Status              string
   118  	TransferProtected   bool
   119  }
   120  
   121  type gdZoneIDName map[string]*gdRecords
   122  
   123  func (z gdZoneIDName) add(zoneID string, zoneRecord *gdRecords) {
   124  	z[zoneID] = zoneRecord
   125  }
   126  
   127  func (z gdZoneIDName) findZoneRecord(hostname string) (suitableZoneID string, suitableZoneRecord *gdRecords) {
   128  	for zoneID, zoneRecord := range z {
   129  		if hostname == zoneRecord.zone || strings.HasSuffix(hostname, "."+zoneRecord.zone) {
   130  			if suitableZoneRecord == nil || len(zoneRecord.zone) > len(suitableZoneRecord.zone) {
   131  				suitableZoneID = zoneID
   132  				suitableZoneRecord = zoneRecord
   133  			}
   134  		}
   135  	}
   136  
   137  	return
   138  }
   139  
   140  // NewGoDaddyProvider initializes a new GoDaddy DNS based Provider.
   141  func NewGoDaddyProvider(ctx context.Context, domainFilter endpoint.DomainFilter, ttl int64, apiKey, apiSecret string, useOTE, dryRun bool) (*GDProvider, error) {
   142  	client, err := NewClient(useOTE, apiKey, apiSecret)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	return &GDProvider{
   148  		client:       client,
   149  		domainFilter: domainFilter,
   150  		ttl:          maxOf(gdMinimalTTL, ttl),
   151  		DryRun:       dryRun,
   152  	}, nil
   153  }
   154  
   155  func (p *GDProvider) zones() ([]string, error) {
   156  	zones := []gdZone{}
   157  	filteredZones := []string{}
   158  
   159  	if err := p.client.Get(domainsURI, &zones); err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	for _, zone := range zones {
   164  		if p.domainFilter.Match(zone.Domain) {
   165  			filteredZones = append(filteredZones, zone.Domain)
   166  			log.Debugf("GoDaddy: %s zone found", zone.Domain)
   167  		}
   168  	}
   169  
   170  	log.Infof("GoDaddy: %d zones found", len(filteredZones))
   171  
   172  	return filteredZones, nil
   173  }
   174  
   175  func (p *GDProvider) zonesRecords(ctx context.Context, all bool) ([]string, []gdRecords, error) {
   176  	var allRecords []gdRecords
   177  	zones, err := p.zones()
   178  	if err != nil {
   179  		return nil, nil, err
   180  	}
   181  
   182  	if len(zones) == 0 {
   183  		allRecords = []gdRecords{}
   184  	} else if len(zones) == 1 {
   185  		record, err := p.records(&ctx, zones[0], all)
   186  		if err != nil {
   187  			return nil, nil, err
   188  		}
   189  
   190  		allRecords = append(allRecords, *record)
   191  	} else {
   192  		chRecords := make(chan gdRecords, len(zones))
   193  
   194  		eg, ctx := errgroup.WithContext(ctx)
   195  
   196  		for _, zoneName := range zones {
   197  			zone := zoneName
   198  			eg.Go(func() error {
   199  				record, err := p.records(&ctx, zone, all)
   200  				if err != nil {
   201  					return err
   202  				}
   203  
   204  				chRecords <- *record
   205  
   206  				return nil
   207  			})
   208  		}
   209  
   210  		if err := eg.Wait(); err != nil {
   211  			return nil, nil, err
   212  		}
   213  
   214  		close(chRecords)
   215  
   216  		for records := range chRecords {
   217  			allRecords = append(allRecords, records)
   218  		}
   219  	}
   220  
   221  	return zones, allRecords, nil
   222  }
   223  
   224  func (p *GDProvider) records(ctx *context.Context, zone string, all bool) (*gdRecords, error) {
   225  	var recordsIds []gdRecordField
   226  
   227  	log.Debugf("GoDaddy: Getting records for %s", zone)
   228  
   229  	if err := p.client.Get(fmt.Sprintf("/v1/domains/%s/records", zone), &recordsIds); err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	if all {
   234  		return &gdRecords{
   235  			zone:    zone,
   236  			records: recordsIds,
   237  		}, nil
   238  	}
   239  
   240  	results := &gdRecords{
   241  		zone:    zone,
   242  		records: make([]gdRecordField, 0, len(recordsIds)),
   243  	}
   244  
   245  	for _, rec := range recordsIds {
   246  		if provider.SupportedRecordType(rec.Type) {
   247  			log.Debugf("GoDaddy: Record %s for %s is %+v", rec.Name, zone, rec)
   248  
   249  			results.records = append(results.records, rec)
   250  		} else {
   251  			log.Infof("GoDaddy: Ignore record %s for %s is %+v", rec.Name, zone, rec)
   252  		}
   253  	}
   254  
   255  	return results, nil
   256  }
   257  
   258  func (p *GDProvider) groupByNameAndType(zoneRecords []gdRecords) []*endpoint.Endpoint {
   259  	endpoints := []*endpoint.Endpoint{}
   260  
   261  	// group supported records by name and type
   262  	groupsByZone := map[string]map[string][]gdRecordField{}
   263  
   264  	for _, zone := range zoneRecords {
   265  		groups := map[string][]gdRecordField{}
   266  
   267  		groupsByZone[zone.zone] = groups
   268  
   269  		for _, r := range zone.records {
   270  			groupBy := fmt.Sprintf("%s - %s", r.Type, r.Name)
   271  
   272  			if _, ok := groups[groupBy]; !ok {
   273  				groups[groupBy] = []gdRecordField{}
   274  			}
   275  
   276  			groups[groupBy] = append(groups[groupBy], r)
   277  		}
   278  	}
   279  
   280  	// create single endpoint with all the targets for each name/type
   281  	for zoneName, groups := range groupsByZone {
   282  		for _, records := range groups {
   283  			targets := []string{}
   284  
   285  			for _, record := range records {
   286  				targets = append(targets, record.Data)
   287  			}
   288  
   289  			var recordName string
   290  
   291  			if records[0].Name == "@" {
   292  				recordName = strings.TrimPrefix(zoneName, ".")
   293  			} else {
   294  				recordName = strings.TrimPrefix(fmt.Sprintf("%s.%s", records[0].Name, zoneName), ".")
   295  			}
   296  
   297  			endpoint := endpoint.NewEndpointWithTTL(
   298  				recordName,
   299  				records[0].Type,
   300  				endpoint.TTL(records[0].TTL),
   301  				targets...,
   302  			)
   303  
   304  			endpoints = append(endpoints, endpoint)
   305  		}
   306  	}
   307  
   308  	return endpoints
   309  }
   310  
   311  // Records returns the list of records in all relevant zones.
   312  func (p *GDProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
   313  	_, records, err := p.zonesRecords(ctx, false)
   314  	if err != nil {
   315  		return nil, err
   316  	}
   317  
   318  	endpoints := p.groupByNameAndType(records)
   319  
   320  	log.Infof("GoDaddy: %d endpoints have been found", len(endpoints))
   321  
   322  	return endpoints, nil
   323  }
   324  
   325  func (p *GDProvider) appendChange(action int, endpoints []*endpoint.Endpoint, allChanges []gdEndpoint) []gdEndpoint {
   326  	for _, e := range endpoints {
   327  		allChanges = append(allChanges, gdEndpoint{
   328  			action:   action,
   329  			endpoint: e,
   330  		})
   331  	}
   332  
   333  	return allChanges
   334  }
   335  
   336  func (p *GDProvider) changeAllRecords(endpoints []gdEndpoint, zoneRecords []*gdRecords) error {
   337  	zoneNameIDMapper := gdZoneIDName{}
   338  
   339  	for _, zoneRecord := range zoneRecords {
   340  		zoneNameIDMapper.add(zoneRecord.zone, zoneRecord)
   341  	}
   342  
   343  	for _, e := range endpoints {
   344  		dnsName := e.endpoint.DNSName
   345  		zone, zoneRecord := zoneNameIDMapper.findZoneRecord(dnsName)
   346  
   347  		if zone == "" {
   348  			log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", dnsName)
   349  		} else {
   350  			dnsName = strings.TrimSuffix(dnsName, "."+zone)
   351  			if dnsName == zone {
   352  				dnsName = ""
   353  			}
   354  
   355  			if e.endpoint.RecordType == endpoint.RecordTypeA && (len(dnsName) == 0) {
   356  				dnsName = "@"
   357  			}
   358  
   359  			e.endpoint.RecordTTL = endpoint.TTL(maxOf(gdMinimalTTL, int64(e.endpoint.RecordTTL)))
   360  
   361  			if err := zoneRecord.applyEndpoint(e.action, p.client, *e.endpoint, dnsName, p.DryRun); err != nil {
   362  				log.Errorf("Unable to apply change %s on record %s type %s, %v", actionNames[e.action], dnsName, e.endpoint.RecordType, err)
   363  
   364  				return err
   365  			}
   366  		}
   367  	}
   368  
   369  	return nil
   370  }
   371  
   372  // ApplyChanges applies a given set of changes in a given zone.
   373  func (p *GDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   374  	if countTargets(changes) == 0 {
   375  		return nil
   376  	}
   377  
   378  	_, records, err := p.zonesRecords(ctx, true)
   379  	if err != nil {
   380  		return err
   381  	}
   382  
   383  	changedZoneRecords := make([]*gdRecords, len(records))
   384  
   385  	for i := range records {
   386  		changedZoneRecords[i] = &records[i]
   387  	}
   388  
   389  	var allChanges []gdEndpoint
   390  
   391  	allChanges = p.appendChange(gdDelete, changes.Delete, allChanges)
   392  
   393  	iOldSkip := make(map[int]bool)
   394  	iNewSkip := make(map[int]bool)
   395  
   396  	for iOld, recOld := range changes.UpdateOld {
   397  		for iNew, recNew := range changes.UpdateNew {
   398  			if recOld.DNSName == recNew.DNSName && recOld.RecordType == recNew.RecordType {
   399  				ReplaceEndpoints := []*endpoint.Endpoint{recNew}
   400  				allChanges = p.appendChange(gdReplace, ReplaceEndpoints, allChanges)
   401  				iOldSkip[iOld] = true
   402  				iNewSkip[iNew] = true
   403  				break
   404  			}
   405  		}
   406  	}
   407  
   408  	for iOld, recOld := range changes.UpdateOld {
   409  		_, found := iOldSkip[iOld]
   410  		if found {
   411  			continue
   412  		}
   413  		for iNew, recNew := range changes.UpdateNew {
   414  			_, found := iNewSkip[iNew]
   415  			if found {
   416  				continue
   417  			}
   418  
   419  			if recOld.DNSName != recNew.DNSName {
   420  				continue
   421  			}
   422  
   423  			DeleteEndpoints := []*endpoint.Endpoint{recOld}
   424  			CreateEndpoints := []*endpoint.Endpoint{recNew}
   425  			allChanges = p.appendChange(gdDelete, DeleteEndpoints, allChanges)
   426  			allChanges = p.appendChange(gdCreate, CreateEndpoints, allChanges)
   427  
   428  			break
   429  		}
   430  	}
   431  
   432  	allChanges = p.appendChange(gdCreate, changes.Create, allChanges)
   433  
   434  	log.Infof("GoDaddy: %d changes will be done", len(allChanges))
   435  
   436  	if err = p.changeAllRecords(allChanges, changedZoneRecords); err != nil {
   437  		return err
   438  	}
   439  
   440  	return nil
   441  }
   442  
   443  func (p *gdRecords) addRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error {
   444  	var response GDErrorResponse
   445  	for _, target := range endpoint.Targets {
   446  		change := gdRecordField{
   447  			Type: endpoint.RecordType,
   448  			Name: dnsName,
   449  			TTL:  int64(endpoint.RecordTTL),
   450  			Data: target,
   451  		}
   452  
   453  		p.records = append(p.records, change)
   454  		p.changed = true
   455  
   456  		log.Debugf("GoDaddy: Add an entry %s to zone %s", change.String(), p.zone)
   457  		if dryRun {
   458  			log.Infof("[DryRun] - Add record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(change))
   459  		} else if err := client.Patch(fmt.Sprintf("/v1/domains/%s/records", p.zone), []gdRecordField{change}, &response); err != nil {
   460  			log.Errorf("Add record %s.%s of type %s failed: %s", change.Name, p.zone, change.Type, response)
   461  
   462  			return err
   463  		}
   464  	}
   465  
   466  	return nil
   467  }
   468  
   469  func (p *gdRecords) replaceRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error {
   470  	changed := []gdReplaceRecordField{}
   471  	records := []string{}
   472  
   473  	for _, target := range endpoint.Targets {
   474  		change := gdRecordField{
   475  			Type: endpoint.RecordType,
   476  			Name: dnsName,
   477  			TTL:  int64(endpoint.RecordTTL),
   478  			Data: target,
   479  		}
   480  
   481  		for index, record := range p.records {
   482  			if record.Type == change.Type && record.Name == change.Name {
   483  				p.records[index] = change
   484  				p.changed = true
   485  			}
   486  		}
   487  		records = append(records, target)
   488  		changed = append(changed, gdReplaceRecordField{
   489  			Data:     change.Data,
   490  			TTL:      change.TTL,
   491  			Port:     change.Port,
   492  			Priority: change.Priority,
   493  			Weight:   change.Weight,
   494  			Protocol: change.Protocol,
   495  			Service:  change.Service,
   496  		})
   497  	}
   498  
   499  	var response GDErrorResponse
   500  
   501  	if dryRun {
   502  		log.Infof("[DryRun] - Replace record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records)
   503  
   504  		return nil
   505  	}
   506  
   507  	log.Debugf("Replace record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records)
   508  	if err := client.Put(fmt.Sprintf("/v1/domains/%s/records/%s/%s", p.zone, endpoint.RecordType, dnsName), changed, &response); err != nil {
   509  		log.Errorf("Replace record %s.%s of type %s failed: %v", dnsName, p.zone, endpoint.RecordType, response)
   510  
   511  		return err
   512  	}
   513  
   514  	return nil
   515  }
   516  
   517  // Remove one record from the record list
   518  func (p *gdRecords) deleteRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error {
   519  	records := []string{}
   520  
   521  	for _, target := range endpoint.Targets {
   522  		change := gdRecordField{
   523  			Type: endpoint.RecordType,
   524  			Name: dnsName,
   525  			TTL:  int64(endpoint.RecordTTL),
   526  			Data: target,
   527  		}
   528  		records = append(records, target)
   529  
   530  		log.Debugf("GoDaddy: Delete an entry %s from zone %s", change.String(), p.zone)
   531  
   532  		deleteIndex := -1
   533  
   534  		for index, record := range p.records {
   535  			if record.Type == change.Type && record.Name == change.Name && record.Data == change.Data {
   536  				deleteIndex = index
   537  				break
   538  			}
   539  		}
   540  
   541  		if deleteIndex >= 0 {
   542  			p.records[deleteIndex] = p.records[len(p.records)-1]
   543  
   544  			p.records = p.records[:len(p.records)-1]
   545  			p.changed = true
   546  		}
   547  	}
   548  
   549  	if dryRun {
   550  		log.Infof("[DryRun] - Delete record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records)
   551  
   552  		return nil
   553  	}
   554  
   555  	var response GDErrorResponse
   556  	if err := client.Delete(fmt.Sprintf("/v1/domains/%s/records/%s/%s", p.zone, endpoint.RecordType, dnsName), &response); err != nil {
   557  		log.Errorf("Delete record %s.%s of type %s failed: %v", dnsName, p.zone, endpoint.RecordType, response)
   558  
   559  		return err
   560  	}
   561  
   562  	return nil
   563  }
   564  
   565  func (p *gdRecords) applyEndpoint(action int, client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error {
   566  	switch action {
   567  	case gdCreate:
   568  		return p.addRecord(client, endpoint, dnsName, dryRun)
   569  	case gdReplace:
   570  		return p.replaceRecord(client, endpoint, dnsName, dryRun)
   571  	case gdDelete:
   572  		return p.deleteRecord(client, endpoint, dnsName, dryRun)
   573  	}
   574  
   575  	return nil
   576  }
   577  
   578  func (c gdRecordField) String() string {
   579  	return fmt.Sprintf("%s %d IN %s %s", c.Name, c.TTL, c.Type, c.Data)
   580  }
   581  
   582  func countTargets(p *plan.Changes) int {
   583  	changes := [][]*endpoint.Endpoint{p.Create, p.UpdateNew, p.UpdateOld, p.Delete}
   584  	count := 0
   585  
   586  	for _, endpoints := range changes {
   587  		for _, endpoint := range endpoints {
   588  			count += len(endpoint.Targets)
   589  		}
   590  	}
   591  
   592  	return count
   593  }
   594  
   595  func maxOf(vars ...int64) int64 {
   596  	max := vars[0]
   597  
   598  	for _, i := range vars {
   599  		if max < i {
   600  			max = i
   601  		}
   602  	}
   603  
   604  	return max
   605  }
   606  
   607  func toString(obj interface{}) string {
   608  	b, err := json.MarshalIndent(obj, "", "	")
   609  	if err != nil {
   610  		return fmt.Sprintf("<%v>", err)
   611  	}
   612  
   613  	return string(b)
   614  }