sigs.k8s.io/external-dns@v0.14.1/provider/rdns/rdns.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 rdns
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"crypto/x509"
    23  	"encoding/json"
    24  	"fmt"
    25  	"math/rand"
    26  	"os"
    27  	"regexp"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/pkg/errors"
    32  	log "github.com/sirupsen/logrus"
    33  	clientv3 "go.etcd.io/etcd/client/v3"
    34  
    35  	"sigs.k8s.io/external-dns/endpoint"
    36  	"sigs.k8s.io/external-dns/plan"
    37  	"sigs.k8s.io/external-dns/provider"
    38  )
    39  
    40  const (
    41  	etcdTimeout       = 5 * time.Second
    42  	rdnsMaxHosts      = 10
    43  	rdnsOriginalLabel = "originalText"
    44  	rdnsPrefix        = "/rdnsv3"
    45  	rdnsTimeout       = 5 * time.Second
    46  )
    47  
    48  func init() {
    49  	rand.New(rand.NewSource(time.Now().UnixNano()))
    50  }
    51  
    52  // RDNSClient is an interface to work with Rancher DNS(RDNS) records in etcdv3 backend.
    53  type RDNSClient interface {
    54  	Get(key string) ([]RDNSRecord, error)
    55  	List(rootDomain string) ([]RDNSRecord, error)
    56  	Set(value RDNSRecord) error
    57  	Delete(key string) error
    58  }
    59  
    60  // RDNSConfig contains configuration to create a new Rancher DNS(RDNS) provider.
    61  type RDNSConfig struct {
    62  	DryRun       bool
    63  	DomainFilter endpoint.DomainFilter
    64  	RootDomain   string
    65  }
    66  
    67  // RDNSProvider is an implementation of Provider for Rancher DNS(RDNS).
    68  type RDNSProvider struct {
    69  	provider.BaseProvider
    70  	client       RDNSClient
    71  	dryRun       bool
    72  	domainFilter endpoint.DomainFilter
    73  	rootDomain   string
    74  }
    75  
    76  // RDNSRecord represents Rancher DNS(RDNS) etcdv3 record.
    77  type RDNSRecord struct {
    78  	AggregationHosts []string `json:"aggregation_hosts,omitempty"`
    79  	Host             string   `json:"host,omitempty"`
    80  	Text             string   `json:"text,omitempty"`
    81  	TTL              uint32   `json:"ttl,omitempty"`
    82  	Key              string   `json:"-"`
    83  }
    84  
    85  // RDNSRecordType represents Rancher DNS(RDNS) etcdv3 record type.
    86  type RDNSRecordType struct {
    87  	Type   string `json:"type,omitempty"`
    88  	Domain string `json:"domain,omitempty"`
    89  }
    90  
    91  type etcdv3Client struct {
    92  	client *clientv3.Client
    93  	ctx    context.Context
    94  }
    95  
    96  var _ RDNSClient = etcdv3Client{}
    97  
    98  // NewRDNSProvider initializes a new Rancher DNS(RDNS) based Provider.
    99  func NewRDNSProvider(config RDNSConfig) (*RDNSProvider, error) {
   100  	client, err := newEtcdv3Client()
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	domain := os.Getenv("RDNS_ROOT_DOMAIN")
   105  	if domain == "" {
   106  		return nil, errors.New("needed root domain environment")
   107  	}
   108  	return &RDNSProvider{
   109  		client:       client,
   110  		dryRun:       config.DryRun,
   111  		domainFilter: config.DomainFilter,
   112  		rootDomain:   domain,
   113  	}, nil
   114  }
   115  
   116  // Records returns all DNS records found in Rancher DNS(RDNS) etcdv3 backend. Depending on the record fields
   117  // it may be mapped to one or two records of type A, TXT, A+TXT.
   118  func (p RDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
   119  	var result []*endpoint.Endpoint
   120  
   121  	rs, err := p.client.List(p.rootDomain)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	for _, r := range rs {
   127  		domains := strings.Split(strings.TrimPrefix(r.Key, rdnsPrefix+"/"), "/")
   128  		keyToDNSNameSplits(domains)
   129  		dnsName := strings.Join(domains, ".")
   130  		if !p.domainFilter.Match(dnsName) {
   131  			continue
   132  		}
   133  
   134  		// only return rdnsMaxHosts at most
   135  		if len(r.AggregationHosts) > 0 {
   136  			if len(r.AggregationHosts) > rdnsMaxHosts {
   137  				r.AggregationHosts = r.AggregationHosts[:rdnsMaxHosts]
   138  			}
   139  			ep := endpoint.NewEndpointWithTTL(
   140  				dnsName,
   141  				endpoint.RecordTypeA,
   142  				endpoint.TTL(r.TTL),
   143  				r.AggregationHosts...,
   144  			)
   145  			ep.Labels[rdnsOriginalLabel] = r.Text
   146  			result = append(result, ep)
   147  		}
   148  		if r.Text != "" {
   149  			ep := endpoint.NewEndpoint(
   150  				dnsName,
   151  				endpoint.RecordTypeTXT,
   152  				r.Text,
   153  			)
   154  			result = append(result, ep)
   155  		}
   156  	}
   157  
   158  	return result, nil
   159  }
   160  
   161  // ApplyChanges stores changes back to etcdv3 converting them to Rancher DNS(RDNS) format and aggregating A and TXT records.
   162  func (p RDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   163  	grouped := map[string][]*endpoint.Endpoint{}
   164  
   165  	for _, ep := range changes.Create {
   166  		grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
   167  	}
   168  
   169  	for _, ep := range changes.UpdateNew {
   170  		if ep.RecordType == endpoint.RecordTypeA {
   171  			// append useless domain records to the changes.Delete
   172  			if err := p.filterAndRemoveUseless(ep, changes); err != nil {
   173  				return err
   174  			}
   175  		}
   176  		grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
   177  	}
   178  
   179  	for dnsName, group := range grouped {
   180  		if !p.domainFilter.Match(dnsName) {
   181  			log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", dnsName)
   182  			continue
   183  		}
   184  
   185  		var rs []RDNSRecord
   186  
   187  		for _, ep := range group {
   188  			if ep.RecordType == endpoint.RecordTypeTXT {
   189  				continue
   190  			}
   191  			for _, target := range ep.Targets {
   192  				rs = append(rs, RDNSRecord{
   193  					Host: target,
   194  					Text: ep.Labels[rdnsOriginalLabel],
   195  					Key:  keyFor(ep.DNSName) + "/" + formatKey(target),
   196  					TTL:  uint32(ep.RecordTTL),
   197  				})
   198  			}
   199  		}
   200  
   201  		// Add the TXT attribute to the existing A record
   202  		for _, ep := range group {
   203  			if ep.RecordType != endpoint.RecordTypeTXT {
   204  				continue
   205  			}
   206  			for i, r := range rs {
   207  				if strings.Contains(r.Key, keyFor(ep.DNSName)) {
   208  					r.Text = ep.Targets[0]
   209  					rs[i] = r
   210  				}
   211  			}
   212  		}
   213  
   214  		for _, r := range rs {
   215  			log.Infof("Add/set key %s to Host=%s, Text=%s, TTL=%d", r.Key, r.Host, r.Text, r.TTL)
   216  			if !p.dryRun {
   217  				err := p.client.Set(r)
   218  				if err != nil {
   219  					return err
   220  				}
   221  			}
   222  		}
   223  	}
   224  
   225  	for _, ep := range changes.Delete {
   226  		key := keyFor(ep.DNSName)
   227  		log.Infof("Delete key %s", key)
   228  		if !p.dryRun {
   229  			err := p.client.Delete(key)
   230  			if err != nil {
   231  				return err
   232  			}
   233  		}
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  // filterAndRemoveUseless filter and remove useless records.
   240  func (p *RDNSProvider) filterAndRemoveUseless(ep *endpoint.Endpoint, changes *plan.Changes) error {
   241  	rs, err := p.client.Get(keyFor(ep.DNSName))
   242  	if err != nil {
   243  		return err
   244  	}
   245  	for _, r := range rs {
   246  		exist := false
   247  		for _, target := range ep.Targets {
   248  			if strings.Contains(r.Key, formatKey(target)) {
   249  				exist = true
   250  				continue
   251  			}
   252  		}
   253  		if !exist {
   254  			ds := strings.Split(strings.TrimPrefix(r.Key, rdnsPrefix+"/"), "/")
   255  			keyToDNSNameSplits(ds)
   256  			changes.Delete = append(changes.Delete, &endpoint.Endpoint{
   257  				DNSName: strings.Join(ds, "."),
   258  			})
   259  		}
   260  	}
   261  	return nil
   262  }
   263  
   264  // newEtcdv3Client is an etcdv3 client constructor.
   265  func newEtcdv3Client() (RDNSClient, error) {
   266  	cfg := &clientv3.Config{}
   267  
   268  	endpoints := os.Getenv("ETCD_URLS")
   269  	ca := os.Getenv("ETCD_CA_FILE")
   270  	cert := os.Getenv("ETCD_CERT_FILE")
   271  	key := os.Getenv("ETCD_KEY_FILE")
   272  	name := os.Getenv("ETCD_TLS_SERVER_NAME")
   273  	insecure := os.Getenv("ETCD_TLS_INSECURE")
   274  
   275  	if endpoints == "" {
   276  		endpoints = "http://localhost:2379"
   277  	}
   278  
   279  	urls := strings.Split(endpoints, ",")
   280  	scheme := strings.ToLower(urls[0])[0:strings.Index(strings.ToLower(urls[0]), "://")]
   281  
   282  	switch scheme {
   283  	case "http":
   284  		cfg.Endpoints = urls
   285  	case "https":
   286  		var certificates []tls.Certificate
   287  
   288  		insecure = strings.ToLower(insecure)
   289  		isInsecure := insecure == "true" || insecure == "yes" || insecure == "1"
   290  
   291  		if ca != "" && key == "" || cert == "" && key != "" {
   292  			return nil, errors.New("either both cert and key or none must be provided")
   293  		}
   294  
   295  		if cert != "" {
   296  			cert, err := tls.LoadX509KeyPair(cert, key)
   297  			if err != nil {
   298  				return nil, fmt.Errorf("could not load TLS cert: %w", err)
   299  			}
   300  			certificates = append(certificates, cert)
   301  		}
   302  
   303  		config := &tls.Config{
   304  			Certificates:       certificates,
   305  			InsecureSkipVerify: isInsecure,
   306  			ServerName:         name,
   307  		}
   308  
   309  		if ca != "" {
   310  			roots := x509.NewCertPool()
   311  			pem, err := os.ReadFile(ca)
   312  			if err != nil {
   313  				return nil, fmt.Errorf("error reading %s: %w", ca, err)
   314  			}
   315  			ok := roots.AppendCertsFromPEM(pem)
   316  			if !ok {
   317  				return nil, fmt.Errorf("could not read root certs: %w", err)
   318  			}
   319  			config.RootCAs = roots
   320  		}
   321  
   322  		cfg.Endpoints = urls
   323  		cfg.TLS = config
   324  	default:
   325  		return nil, errors.New("etcdv3 URLs must start with either http:// or https://")
   326  	}
   327  
   328  	c, err := clientv3.New(*cfg)
   329  	if err != nil {
   330  		return nil, err
   331  	}
   332  
   333  	return etcdv3Client{c, context.Background()}, nil
   334  }
   335  
   336  // Get return A records stored in etcdv3 stored anywhere under the given key (recursively).
   337  func (c etcdv3Client) Get(key string) ([]RDNSRecord, error) {
   338  	ctx, cancel := context.WithTimeout(c.ctx, rdnsTimeout)
   339  	defer cancel()
   340  
   341  	result, err := c.client.Get(ctx, key, clientv3.WithPrefix())
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  
   346  	rs := make([]RDNSRecord, 0)
   347  	for _, v := range result.Kvs {
   348  		r := new(RDNSRecord)
   349  		if err := json.Unmarshal(v.Value, r); err != nil {
   350  			return nil, fmt.Errorf("%s: %w", v.Key, err)
   351  		}
   352  		r.Key = string(v.Key)
   353  		rs = append(rs, *r)
   354  	}
   355  
   356  	return rs, nil
   357  }
   358  
   359  // List return all records stored in etcdv3 stored anywhere under the given rootDomain (recursively).
   360  func (c etcdv3Client) List(rootDomain string) ([]RDNSRecord, error) {
   361  	ctx, cancel := context.WithTimeout(c.ctx, rdnsTimeout)
   362  	defer cancel()
   363  
   364  	path := keyFor(rootDomain)
   365  
   366  	result, err := c.client.Get(ctx, path, clientv3.WithPrefix())
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  
   371  	return c.aggregationRecords(result)
   372  }
   373  
   374  // Set persists records data into etcdv3.
   375  func (c etcdv3Client) Set(r RDNSRecord) error {
   376  	ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
   377  	defer cancel()
   378  
   379  	v, err := json.Marshal(&r)
   380  	if err != nil {
   381  		return err
   382  	}
   383  
   384  	if r.Text == "" && r.Host == "" {
   385  		return nil
   386  	}
   387  
   388  	_, err = c.client.Put(ctx, r.Key, string(v))
   389  	if err != nil {
   390  		return err
   391  	}
   392  
   393  	return nil
   394  }
   395  
   396  // Delete deletes record from etcdv3.
   397  func (c etcdv3Client) Delete(key string) error {
   398  	ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
   399  	defer cancel()
   400  
   401  	_, err := c.client.Delete(ctx, key, clientv3.WithPrefix())
   402  	return err
   403  }
   404  
   405  // aggregationRecords will aggregation multi A records under the given path.
   406  // e.g. A: 1_1_1_1.xxx.lb.rancher.cloud & 2_2_2_2.sample.lb.rancher.cloud => sample.lb.rancher.cloud {"aggregation_hosts": ["1.1.1.1", "2.2.2.2"]}
   407  // e.g. TXT: sample.lb.rancher.cloud => sample.lb.rancher.cloud => {"text": "xxx"}
   408  func (c etcdv3Client) aggregationRecords(result *clientv3.GetResponse) ([]RDNSRecord, error) {
   409  	var rs []RDNSRecord
   410  	bx := make(map[RDNSRecordType]RDNSRecord)
   411  
   412  	for _, n := range result.Kvs {
   413  		r := new(RDNSRecord)
   414  		if err := json.Unmarshal(n.Value, r); err != nil {
   415  			return nil, fmt.Errorf("%s: %w", n.Key, err)
   416  		}
   417  
   418  		r.Key = string(n.Key)
   419  
   420  		if r.Host == "" && r.Text == "" {
   421  			continue
   422  		}
   423  
   424  		if r.Host != "" {
   425  			c := RDNSRecord{
   426  				AggregationHosts: r.AggregationHosts,
   427  				Host:             r.Host,
   428  				Text:             r.Text,
   429  				TTL:              r.TTL,
   430  				Key:              r.Key,
   431  			}
   432  			n, isContinue := appendRecords(c, endpoint.RecordTypeA, bx, rs)
   433  			if isContinue {
   434  				continue
   435  			}
   436  			rs = n
   437  		}
   438  
   439  		if r.Text != "" && r.Host == "" {
   440  			c := RDNSRecord{
   441  				AggregationHosts: []string{},
   442  				Host:             r.Host,
   443  				Text:             r.Text,
   444  				TTL:              r.TTL,
   445  				Key:              r.Key,
   446  			}
   447  			n, isContinue := appendRecords(c, endpoint.RecordTypeTXT, bx, rs)
   448  			if isContinue {
   449  				continue
   450  			}
   451  			rs = n
   452  		}
   453  	}
   454  
   455  	return rs, nil
   456  }
   457  
   458  // appendRecords append record to an array
   459  func appendRecords(r RDNSRecord, dnsType string, bx map[RDNSRecordType]RDNSRecord, rs []RDNSRecord) ([]RDNSRecord, bool) {
   460  	dnsName := keyToParentDNSName(r.Key)
   461  	bt := RDNSRecordType{Domain: dnsName, Type: dnsType}
   462  	if v, ok := bx[bt]; ok {
   463  		// skip the TXT records if already added to record list.
   464  		// append A record if dnsName already added to record list but not found the value.
   465  		// the same record might be found in multiple etcdv3 nodes.
   466  		if bt.Type == endpoint.RecordTypeA {
   467  			exist := false
   468  			for _, h := range v.AggregationHosts {
   469  				if h == r.Host {
   470  					exist = true
   471  					break
   472  				}
   473  			}
   474  			if !exist {
   475  				for i, t := range rs {
   476  					if !strings.HasPrefix(r.Key, t.Key) {
   477  						continue
   478  					}
   479  					t.Host = ""
   480  					t.AggregationHosts = append(t.AggregationHosts, r.Host)
   481  					bx[bt] = t
   482  					rs[i] = t
   483  				}
   484  			}
   485  		}
   486  		return rs, true
   487  	}
   488  
   489  	if bt.Type == endpoint.RecordTypeA {
   490  		r.AggregationHosts = append(r.AggregationHosts, r.Host)
   491  	}
   492  
   493  	r.Key = rdnsPrefix + dnsNameToKey(dnsName)
   494  	r.Host = ""
   495  	bx[bt] = r
   496  	rs = append(rs, r)
   497  	return rs, false
   498  }
   499  
   500  // keyFor used to get a path as etcdv3 preferred.
   501  // e.g. sample.lb.rancher.cloud => /rdnsv3/cloud/rancher/lb/sample
   502  func keyFor(fqdn string) string {
   503  	return rdnsPrefix + dnsNameToKey(fqdn)
   504  }
   505  
   506  // keyToParentDNSName used to get dnsName.
   507  // e.g. /rdnsv3/cloud/rancher/lb/sample/xxx => xxx.sample.lb.rancher.cloud
   508  // e.g. /rdnsv3/cloud/rancher/lb/sample/xxx/1_1_1_1 => xxx.sample.lb.rancher.cloud
   509  func keyToParentDNSName(key string) string {
   510  	ds := strings.Split(strings.TrimPrefix(key, rdnsPrefix+"/"), "/")
   511  	keyToDNSNameSplits(ds)
   512  
   513  	dns := strings.Join(ds, ".")
   514  	prefix := strings.Split(dns, ".")[0]
   515  
   516  	p := `^\d{1,3}_\d{1,3}_\d{1,3}_\d{1,3}$`
   517  	m, _ := regexp.MatchString(p, prefix)
   518  	if prefix != "" && strings.Contains(prefix, "_") && m {
   519  		// 1_1_1_1.xxx.sample.lb.rancher.cloud => xxx.sample.lb.rancher.cloud
   520  		return strings.Join(strings.Split(dns, ".")[1:], ".")
   521  	}
   522  
   523  	return dns
   524  }
   525  
   526  // dnsNameToKey used to convert domain to a path as etcdv3 preferred.
   527  // e.g. sample.lb.rancher.cloud => /cloud/rancher/lb/sample
   528  func dnsNameToKey(domain string) string {
   529  	ss := strings.Split(domain, ".")
   530  	last := len(ss) - 1
   531  	for i := 0; i < len(ss)/2; i++ {
   532  		ss[i], ss[last-i] = ss[last-i], ss[i]
   533  	}
   534  	return "/" + strings.Join(ss, "/")
   535  }
   536  
   537  // keyToDNSNameSplits used to reverse etcdv3 path to domain splits.
   538  // e.g. /cloud/rancher/lb/sample => [sample lb rancher cloud]
   539  func keyToDNSNameSplits(ss []string) {
   540  	for i := 0; i < len(ss)/2; i++ {
   541  		j := len(ss) - i - 1
   542  		ss[i], ss[j] = ss[j], ss[i]
   543  	}
   544  }
   545  
   546  // formatKey used to format a key as etcdv3 preferred
   547  // e.g. 1.1.1.1 => 1_1_1_1
   548  // e.g. sample.lb.rancher.cloud => sample_lb_rancher_cloud
   549  func formatKey(key string) string {
   550  	return strings.Replace(key, ".", "_", -1)
   551  }