sigs.k8s.io/external-dns@v0.14.1/provider/coredns/coredns.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 coredns
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"crypto/x509"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"math/rand"
    27  	"net"
    28  	"os"
    29  	"strings"
    30  	"time"
    31  
    32  	log "github.com/sirupsen/logrus"
    33  	etcdcv3 "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  	priority    = 10 // default priority when nothing is set
    42  	etcdTimeout = 5 * time.Second
    43  
    44  	randomPrefixLabel = "prefix"
    45  )
    46  
    47  // coreDNSClient is an interface to work with CoreDNS service records in etcd
    48  type coreDNSClient interface {
    49  	GetServices(prefix string) ([]*Service, error)
    50  	SaveService(value *Service) error
    51  	DeleteService(key string) error
    52  }
    53  
    54  type coreDNSProvider struct {
    55  	provider.BaseProvider
    56  	dryRun        bool
    57  	coreDNSPrefix string
    58  	domainFilter  endpoint.DomainFilter
    59  	client        coreDNSClient
    60  }
    61  
    62  // Service represents CoreDNS etcd record
    63  type Service struct {
    64  	Host     string `json:"host,omitempty"`
    65  	Port     int    `json:"port,omitempty"`
    66  	Priority int    `json:"priority,omitempty"`
    67  	Weight   int    `json:"weight,omitempty"`
    68  	Text     string `json:"text,omitempty"`
    69  	Mail     bool   `json:"mail,omitempty"` // Be an MX record. Priority becomes Preference.
    70  	TTL      uint32 `json:"ttl,omitempty"`
    71  
    72  	// When a SRV record with a "Host: IP-address" is added, we synthesize
    73  	// a srv.Target domain name.  Normally we convert the full Key where
    74  	// the record lives to a DNS name and use this as the srv.Target.  When
    75  	// TargetStrip > 0 we strip the left most TargetStrip labels from the
    76  	// DNS name.
    77  	TargetStrip int `json:"targetstrip,omitempty"`
    78  
    79  	// Group is used to group (or *not* to group) different services
    80  	// together. Services with an identical Group are returned in the same
    81  	// answer.
    82  	Group string `json:"group,omitempty"`
    83  
    84  	// Etcd key where we found this service and ignored from json un-/marshaling
    85  	Key string `json:"-"`
    86  }
    87  
    88  type etcdClient struct {
    89  	client *etcdcv3.Client
    90  	ctx    context.Context
    91  }
    92  
    93  var _ coreDNSClient = etcdClient{}
    94  
    95  // GetService return all Service records stored in etcd stored anywhere under the given key (recursively)
    96  func (c etcdClient) GetServices(prefix string) ([]*Service, error) {
    97  	ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
    98  	defer cancel()
    99  
   100  	path := prefix
   101  	r, err := c.client.Get(ctx, path, etcdcv3.WithPrefix())
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	var svcs []*Service
   107  	bx := make(map[Service]bool)
   108  	for _, n := range r.Kvs {
   109  		svc := new(Service)
   110  		if err := json.Unmarshal(n.Value, svc); err != nil {
   111  			return nil, fmt.Errorf("%s: %w", n.Key, err)
   112  		}
   113  		b := Service{Host: svc.Host, Port: svc.Port, Priority: svc.Priority, Weight: svc.Weight, Text: svc.Text, Key: string(n.Key)}
   114  		if _, ok := bx[b]; ok {
   115  			// skip the service if already added to service list.
   116  			// the same service might be found in multiple etcd nodes.
   117  			continue
   118  		}
   119  		bx[b] = true
   120  
   121  		svc.Key = string(n.Key)
   122  		if svc.Priority == 0 {
   123  			svc.Priority = priority
   124  		}
   125  		svcs = append(svcs, svc)
   126  	}
   127  
   128  	return svcs, nil
   129  }
   130  
   131  // SaveService persists service data into etcd
   132  func (c etcdClient) SaveService(service *Service) error {
   133  	ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
   134  	defer cancel()
   135  
   136  	value, err := json.Marshal(&service)
   137  	if err != nil {
   138  		return err
   139  	}
   140  	_, err = c.client.Put(ctx, service.Key, string(value))
   141  	if err != nil {
   142  		return err
   143  	}
   144  	return nil
   145  }
   146  
   147  // DeleteService deletes service record from etcd
   148  func (c etcdClient) DeleteService(key string) error {
   149  	ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
   150  	defer cancel()
   151  
   152  	_, err := c.client.Delete(ctx, key, etcdcv3.WithPrefix())
   153  	return err
   154  }
   155  
   156  // loads TLS artifacts and builds tls.Config object
   157  func newTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool) (*tls.Config, error) {
   158  	if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" {
   159  		return nil, errors.New("either both cert and key or none must be provided")
   160  	}
   161  	var certificates []tls.Certificate
   162  	if certPath != "" {
   163  		cert, err := tls.LoadX509KeyPair(certPath, keyPath)
   164  		if err != nil {
   165  			return nil, fmt.Errorf("could not load TLS cert: %w", err)
   166  		}
   167  		certificates = append(certificates, cert)
   168  	}
   169  	roots, err := loadRoots(caPath)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	return &tls.Config{
   175  		Certificates:       certificates,
   176  		RootCAs:            roots,
   177  		InsecureSkipVerify: insecure,
   178  		ServerName:         serverName,
   179  	}, nil
   180  }
   181  
   182  // loads CA cert
   183  func loadRoots(caPath string) (*x509.CertPool, error) {
   184  	if caPath == "" {
   185  		return nil, nil
   186  	}
   187  
   188  	roots := x509.NewCertPool()
   189  	pem, err := os.ReadFile(caPath)
   190  	if err != nil {
   191  		return nil, fmt.Errorf("error reading %s: %w", caPath, err)
   192  	}
   193  	ok := roots.AppendCertsFromPEM(pem)
   194  	if !ok {
   195  		return nil, fmt.Errorf("could not read root certs: %w", err)
   196  	}
   197  	return roots, nil
   198  }
   199  
   200  // builds etcd client config depending on connection scheme and TLS parameters
   201  func getETCDConfig() (*etcdcv3.Config, error) {
   202  	etcdURLsStr := os.Getenv("ETCD_URLS")
   203  	if etcdURLsStr == "" {
   204  		etcdURLsStr = "http://localhost:2379"
   205  	}
   206  	etcdURLs := strings.Split(etcdURLsStr, ",")
   207  	firstURL := strings.ToLower(etcdURLs[0])
   208  	if strings.HasPrefix(firstURL, "http://") {
   209  		return &etcdcv3.Config{Endpoints: etcdURLs}, nil
   210  	} else if strings.HasPrefix(firstURL, "https://") {
   211  		caFile := os.Getenv("ETCD_CA_FILE")
   212  		certFile := os.Getenv("ETCD_CERT_FILE")
   213  		keyFile := os.Getenv("ETCD_KEY_FILE")
   214  		serverName := os.Getenv("ETCD_TLS_SERVER_NAME")
   215  		isInsecureStr := strings.ToLower(os.Getenv("ETCD_TLS_INSECURE"))
   216  		isInsecure := isInsecureStr == "true" || isInsecureStr == "yes" || isInsecureStr == "1"
   217  		tlsConfig, err := newTLSConfig(certFile, keyFile, caFile, serverName, isInsecure)
   218  		if err != nil {
   219  			return nil, err
   220  		}
   221  		return &etcdcv3.Config{
   222  			Endpoints: etcdURLs,
   223  			TLS:       tlsConfig,
   224  		}, nil
   225  	} else {
   226  		return nil, errors.New("etcd URLs must start with either http:// or https://")
   227  	}
   228  }
   229  
   230  // newETCDClient is an etcd client constructor
   231  func newETCDClient() (coreDNSClient, error) {
   232  	cfg, err := getETCDConfig()
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  	c, err := etcdcv3.New(*cfg)
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  	return etcdClient{c, context.Background()}, nil
   241  }
   242  
   243  // NewCoreDNSProvider is a CoreDNS provider constructor
   244  func NewCoreDNSProvider(domainFilter endpoint.DomainFilter, prefix string, dryRun bool) (provider.Provider, error) {
   245  	client, err := newETCDClient()
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	return coreDNSProvider{
   251  		client:        client,
   252  		dryRun:        dryRun,
   253  		coreDNSPrefix: prefix,
   254  		domainFilter:  domainFilter,
   255  	}, nil
   256  }
   257  
   258  // findEp takes an Endpoint slice and looks for an element in it. If found it will
   259  // return Endpoint, otherwise it will return nil and a bool of false.
   260  func findEp(slice []*endpoint.Endpoint, dnsName string) (*endpoint.Endpoint, bool) {
   261  	for _, item := range slice {
   262  		if item.DNSName == dnsName {
   263  			return item, true
   264  		}
   265  	}
   266  	return nil, false
   267  }
   268  
   269  // findLabelInTargets takes an ep.Targets string slice and looks for an element in it. If found it will
   270  // return its string value, otherwise it will return empty string and a bool of false.
   271  func findLabelInTargets(targets []string, label string) (string, bool) {
   272  	for _, target := range targets {
   273  		if target == label {
   274  			return target, true
   275  		}
   276  	}
   277  	return "", false
   278  }
   279  
   280  // Records returns all DNS records found in CoreDNS etcd backend. Depending on the record fields
   281  // it may be mapped to one or two records of type A, CNAME, TXT, A+TXT, CNAME+TXT
   282  func (p coreDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
   283  	var result []*endpoint.Endpoint
   284  	services, err := p.client.GetServices(p.coreDNSPrefix)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  	for _, service := range services {
   289  		domains := strings.Split(strings.TrimPrefix(service.Key, p.coreDNSPrefix), "/")
   290  		reverse(domains)
   291  		dnsName := strings.Join(domains[service.TargetStrip:], ".")
   292  		if !p.domainFilter.Match(dnsName) {
   293  			continue
   294  		}
   295  		log.Debugf("Getting service (%v) with service host (%s)", service, service.Host)
   296  		prefix := strings.Join(domains[:service.TargetStrip], ".")
   297  		if service.Host != "" {
   298  			ep, found := findEp(result, dnsName)
   299  			if found {
   300  				ep.Targets = append(ep.Targets, service.Host)
   301  				log.Debugf("Extending ep (%s) with new service host (%s)", ep, service.Host)
   302  			} else {
   303  				ep = endpoint.NewEndpointWithTTL(
   304  					dnsName,
   305  					guessRecordType(service.Host),
   306  					endpoint.TTL(service.TTL),
   307  					service.Host,
   308  				)
   309  				log.Debugf("Creating new ep (%s) with new service host (%s)", ep, service.Host)
   310  			}
   311  			ep.Labels["originalText"] = service.Text
   312  			ep.Labels[randomPrefixLabel] = prefix
   313  			ep.Labels[service.Host] = prefix
   314  			result = append(result, ep)
   315  		}
   316  		if service.Text != "" {
   317  			ep := endpoint.NewEndpoint(
   318  				dnsName,
   319  				endpoint.RecordTypeTXT,
   320  				service.Text,
   321  			)
   322  			ep.Labels[randomPrefixLabel] = prefix
   323  			result = append(result, ep)
   324  		}
   325  	}
   326  	return result, nil
   327  }
   328  
   329  // ApplyChanges stores changes back to etcd converting them to CoreDNS format and aggregating A/CNAME and TXT records
   330  func (p coreDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   331  	grouped := map[string][]*endpoint.Endpoint{}
   332  	for _, ep := range changes.Create {
   333  		grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
   334  	}
   335  	for i, ep := range changes.UpdateNew {
   336  		ep.Labels = changes.UpdateOld[i].Labels
   337  		log.Debugf("Updating labels (%s) with old labels(%s)", ep.Labels, changes.UpdateOld[i].Labels)
   338  		grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
   339  	}
   340  	for dnsName, group := range grouped {
   341  		if !p.domainFilter.Match(dnsName) {
   342  			log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", dnsName)
   343  			continue
   344  		}
   345  		var services []Service
   346  		for _, ep := range group {
   347  			if ep.RecordType == endpoint.RecordTypeTXT {
   348  				continue
   349  			}
   350  
   351  			for _, target := range ep.Targets {
   352  				prefix := ep.Labels[target]
   353  				log.Debugf("Getting prefix(%s) from label(%s)", prefix, target)
   354  				if prefix == "" {
   355  					prefix = fmt.Sprintf("%08x", rand.Int31())
   356  					log.Infof("Generating new prefix: (%s)", prefix)
   357  				}
   358  
   359  				service := Service{
   360  					Host:        target,
   361  					Text:        ep.Labels["originalText"],
   362  					Key:         p.etcdKeyFor(prefix + "." + dnsName),
   363  					TargetStrip: strings.Count(prefix, ".") + 1,
   364  					TTL:         uint32(ep.RecordTTL),
   365  				}
   366  				services = append(services, service)
   367  				ep.Labels[target] = prefix
   368  				log.Debugf("Putting prefix(%s) to label(%s)", prefix, target)
   369  				log.Debugf("Ep labels structure now: (%v)", ep.Labels)
   370  			}
   371  
   372  			// Clean outdated targets
   373  			for label, labelPrefix := range ep.Labels {
   374  				// Skip non Target related labels
   375  				labelsToSkip := []string{"originalText", "prefix", "resource"}
   376  				if _, ok := findLabelInTargets(labelsToSkip, label); ok {
   377  					continue
   378  				}
   379  
   380  				log.Debugf("Finding label (%s) in targets(%v)", label, ep.Targets)
   381  				if _, ok := findLabelInTargets(ep.Targets, label); !ok {
   382  					log.Debugf("Found non existing label(%s) in targets(%v)", label, ep.Targets)
   383  					dnsName := ep.DNSName
   384  					dnsName = labelPrefix + "." + dnsName
   385  					key := p.etcdKeyFor(dnsName)
   386  					log.Infof("Delete key %s", key)
   387  					if !p.dryRun {
   388  						err := p.client.DeleteService(key)
   389  						if err != nil {
   390  							return err
   391  						}
   392  					}
   393  				}
   394  			}
   395  		}
   396  		index := 0
   397  		for _, ep := range group {
   398  			if ep.RecordType != endpoint.RecordTypeTXT {
   399  				continue
   400  			}
   401  			if index >= len(services) {
   402  				prefix := ep.Labels[randomPrefixLabel]
   403  				if prefix == "" {
   404  					prefix = fmt.Sprintf("%08x", rand.Int31())
   405  				}
   406  				services = append(services, Service{
   407  					Key:         p.etcdKeyFor(prefix + "." + dnsName),
   408  					TargetStrip: strings.Count(prefix, ".") + 1,
   409  					TTL:         uint32(ep.RecordTTL),
   410  				})
   411  			}
   412  			services[index].Text = ep.Targets[0]
   413  			index++
   414  		}
   415  
   416  		for i := index; index > 0 && i < len(services); i++ {
   417  			services[i].Text = ""
   418  		}
   419  
   420  		for _, service := range services {
   421  			log.Infof("Add/set key %s to Host=%s, Text=%s, TTL=%d", service.Key, service.Host, service.Text, service.TTL)
   422  			if !p.dryRun {
   423  				err := p.client.SaveService(&service)
   424  				if err != nil {
   425  					return err
   426  				}
   427  			}
   428  		}
   429  	}
   430  
   431  	for _, ep := range changes.Delete {
   432  		dnsName := ep.DNSName
   433  		if ep.Labels[randomPrefixLabel] != "" {
   434  			dnsName = ep.Labels[randomPrefixLabel] + "." + dnsName
   435  		}
   436  		key := p.etcdKeyFor(dnsName)
   437  		log.Infof("Delete key %s", key)
   438  		if !p.dryRun {
   439  			err := p.client.DeleteService(key)
   440  			if err != nil {
   441  				return err
   442  			}
   443  		}
   444  	}
   445  
   446  	return nil
   447  }
   448  
   449  func (p coreDNSProvider) etcdKeyFor(dnsName string) string {
   450  	domains := strings.Split(dnsName, ".")
   451  	reverse(domains)
   452  	return p.coreDNSPrefix + strings.Join(domains, "/")
   453  }
   454  
   455  func guessRecordType(target string) string {
   456  	if net.ParseIP(target) != nil {
   457  		return endpoint.RecordTypeA
   458  	}
   459  	return endpoint.RecordTypeCNAME
   460  }
   461  
   462  func reverse(slice []string) {
   463  	for i := 0; i < len(slice)/2; i++ {
   464  		j := len(slice) - i - 1
   465  		slice[i], slice[j] = slice[j], slice[i]
   466  	}
   467  }