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

     1  /*
     2  Copyright 2019 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 rcode0
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/url"
    23  	"os"
    24  	"strings"
    25  
    26  	rc0 "github.com/nic-at/rc0go"
    27  	log "github.com/sirupsen/logrus"
    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  // RcodeZeroProvider implements the DNS provider for RcodeZero Anycast DNS.
    35  type RcodeZeroProvider struct {
    36  	provider.BaseProvider
    37  	Client *rc0.Client
    38  
    39  	DomainFilter endpoint.DomainFilter
    40  	DryRun       bool
    41  	TXTEncrypt   bool
    42  	Key          []byte
    43  }
    44  
    45  // NewRcodeZeroProvider creates a new RcodeZero Anycast DNS provider.
    46  //
    47  // Returns the provider or an error if a provider could not be created.
    48  func NewRcodeZeroProvider(domainFilter endpoint.DomainFilter, dryRun bool, txtEnc bool) (*RcodeZeroProvider, error) {
    49  	client, err := rc0.NewClient(os.Getenv("RC0_API_KEY"))
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	value := os.Getenv("RC0_BASE_URL")
    55  	if len(value) != 0 {
    56  		client.BaseURL, err = url.Parse(os.Getenv("RC0_BASE_URL"))
    57  	}
    58  
    59  	if err != nil {
    60  		return nil, fmt.Errorf("failed to initialize rcodezero provider: %v", err)
    61  	}
    62  
    63  	provider := &RcodeZeroProvider{
    64  		Client:       client,
    65  		DomainFilter: domainFilter,
    66  		DryRun:       dryRun,
    67  		TXTEncrypt:   txtEnc,
    68  	}
    69  
    70  	if txtEnc {
    71  		provider.Key = []byte(os.Getenv("RC0_ENC_KEY"))
    72  	}
    73  
    74  	return provider, nil
    75  }
    76  
    77  // Zones returns filtered zones if filter is set
    78  func (p *RcodeZeroProvider) Zones() ([]*rc0.Zone, error) {
    79  	var result []*rc0.Zone
    80  
    81  	zones, err := p.fetchZones()
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  
    86  	for _, zone := range zones {
    87  		if p.DomainFilter.Match(zone.Domain) {
    88  			result = append(result, zone)
    89  		}
    90  	}
    91  
    92  	return result, nil
    93  }
    94  
    95  // Records returns resource records
    96  //
    97  // Decrypts TXT records if TXT-Encrypt flag is set and key is provided
    98  func (p *RcodeZeroProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
    99  	zones, err := p.Zones()
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	var endpoints []*endpoint.Endpoint
   105  
   106  	for _, zone := range zones {
   107  		rrset, err := p.fetchRecords(zone.Domain)
   108  		if err != nil {
   109  			return nil, err
   110  		}
   111  
   112  		for _, r := range rrset {
   113  			if provider.SupportedRecordType(r.Type) {
   114  				if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(r.Type, "TXT") {
   115  					p.Client.RRSet.DecryptTXT(p.Key, r)
   116  				}
   117  				if len(r.Records) > 1 {
   118  					for _, _r := range r.Records {
   119  						if !_r.Disabled {
   120  							endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), _r.Content))
   121  						}
   122  					}
   123  				} else if !r.Records[0].Disabled {
   124  					endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), r.Records[0].Content))
   125  				}
   126  			}
   127  		}
   128  	}
   129  
   130  	return endpoints, nil
   131  }
   132  
   133  // ApplyChanges applies a given set of changes in a given zone.
   134  func (p *RcodeZeroProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   135  	combinedChanges := make([]*rc0.RRSetChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
   136  
   137  	combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeADD, changes.Create)...)
   138  	combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeUPDATE, changes.UpdateNew)...)
   139  	combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeDELETE, changes.Delete)...)
   140  
   141  	return p.submitChanges(combinedChanges)
   142  }
   143  
   144  // Helper function
   145  func rcodezeroChangesByZone(zones []*rc0.Zone, changeSet []*rc0.RRSetChange) map[string][]*rc0.RRSetChange {
   146  	changes := make(map[string][]*rc0.RRSetChange)
   147  	zoneNameIDMapper := provider.ZoneIDName{}
   148  	for _, z := range zones {
   149  		zoneNameIDMapper.Add(z.Domain, z.Domain)
   150  		changes[z.Domain] = []*rc0.RRSetChange{}
   151  	}
   152  
   153  	for _, c := range changeSet {
   154  		zone, _ := zoneNameIDMapper.FindZone(c.Name)
   155  		if zone == "" {
   156  			log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.Name)
   157  			continue
   158  		}
   159  		changes[zone] = append(changes[zone], c)
   160  	}
   161  
   162  	return changes
   163  }
   164  
   165  // Helper function
   166  func (p *RcodeZeroProvider) fetchRecords(zoneName string) ([]*rc0.RRType, error) {
   167  	var allRecords []*rc0.RRType
   168  
   169  	listOptions := rc0.NewListOptions()
   170  
   171  	for {
   172  		records, page, err := p.Client.RRSet.List(zoneName, listOptions)
   173  		if err != nil {
   174  			return nil, err
   175  		}
   176  
   177  		allRecords = append(allRecords, records...)
   178  
   179  		if page == nil || (page.CurrentPage == page.LastPage) {
   180  			break
   181  		}
   182  
   183  		listOptions.SetPageNumber(page.CurrentPage + 1)
   184  	}
   185  
   186  	return allRecords, nil
   187  }
   188  
   189  // Helper function
   190  func (p *RcodeZeroProvider) fetchZones() ([]*rc0.Zone, error) {
   191  	var allZones []*rc0.Zone
   192  
   193  	listOptions := rc0.NewListOptions()
   194  
   195  	for {
   196  		zones, page, err := p.Client.Zones.List(listOptions)
   197  		if err != nil {
   198  			return nil, err
   199  		}
   200  		allZones = append(allZones, zones...)
   201  
   202  		if page == nil || page.IsLastPage() {
   203  			break
   204  		}
   205  
   206  		listOptions.SetPageNumber(page.CurrentPage + 1)
   207  	}
   208  
   209  	return allZones, nil
   210  }
   211  
   212  // Helper function to submit changes.
   213  //
   214  // Changes are submitted by change type.
   215  func (p *RcodeZeroProvider) submitChanges(changes []*rc0.RRSetChange) error {
   216  	if len(changes) == 0 {
   217  		return nil
   218  	}
   219  
   220  	zones, err := p.Zones()
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	// separate into per-zone change sets to be passed to the API.
   226  	changesByZone := rcodezeroChangesByZone(zones, changes)
   227  	for zoneName, changes := range changesByZone {
   228  		for _, change := range changes {
   229  			logFields := log.Fields{
   230  				"record":  change.Name,
   231  				"content": change.Records[0].Content,
   232  				"type":    change.Type,
   233  				"action":  change.ChangeType,
   234  				"zone":    zoneName,
   235  			}
   236  
   237  			log.WithFields(logFields).Info("Changing record.")
   238  
   239  			if p.DryRun {
   240  				continue
   241  			}
   242  
   243  			// to avoid accidentally adding extra dot if already present
   244  			change.Name = strings.TrimSuffix(change.Name, ".") + "."
   245  
   246  			switch change.ChangeType {
   247  			case rc0.ChangeTypeADD:
   248  				sr, err := p.Client.RRSet.Create(zoneName, []*rc0.RRSetChange{change})
   249  				if err != nil {
   250  					return err
   251  				}
   252  
   253  				if sr.HasError() {
   254  					return fmt.Errorf("adding new RR resulted in an error: %v", sr.Message)
   255  				}
   256  
   257  			case rc0.ChangeTypeUPDATE:
   258  				sr, err := p.Client.RRSet.Edit(zoneName, []*rc0.RRSetChange{change})
   259  				if err != nil {
   260  					return err
   261  				}
   262  
   263  				if sr.HasError() {
   264  					return fmt.Errorf("updating existing RR resulted in an error: %v", sr.Message)
   265  				}
   266  
   267  			case rc0.ChangeTypeDELETE:
   268  				sr, err := p.Client.RRSet.Delete(zoneName, []*rc0.RRSetChange{change})
   269  				if err != nil {
   270  					return err
   271  				}
   272  
   273  				if sr.HasError() {
   274  					return fmt.Errorf("deleting existing RR resulted in an error: %v", sr.Message)
   275  				}
   276  
   277  			default:
   278  				return fmt.Errorf("unsupported changeType submitted: %v", change.ChangeType)
   279  			}
   280  		}
   281  	}
   282  	return nil
   283  }
   284  
   285  // NewRcodezeroChanges returns a RcodeZero specific array with rrset change objects.
   286  func (p *RcodeZeroProvider) NewRcodezeroChanges(action string, endpoints []*endpoint.Endpoint) []*rc0.RRSetChange {
   287  	changes := make([]*rc0.RRSetChange, 0, len(endpoints))
   288  
   289  	for _, _endpoint := range endpoints {
   290  		changes = append(changes, p.NewRcodezeroChange(action, _endpoint))
   291  	}
   292  
   293  	return changes
   294  }
   295  
   296  // NewRcodezeroChange returns a RcodeZero specific rrset change object.
   297  func (p *RcodeZeroProvider) NewRcodezeroChange(action string, endpoint *endpoint.Endpoint) *rc0.RRSetChange {
   298  	change := &rc0.RRSetChange{
   299  		Type:       endpoint.RecordType,
   300  		ChangeType: action,
   301  		Name:       endpoint.DNSName,
   302  		Records: []*rc0.Record{{
   303  			Disabled: false,
   304  			Content:  endpoint.Targets[0],
   305  		}},
   306  	}
   307  
   308  	if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(endpoint.RecordType, "TXT") {
   309  		p.Client.RRSet.EncryptTXT(p.Key, change)
   310  	}
   311  
   312  	return change
   313  }