github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/config/dns/etcd_dns.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package dns
    19  
    20  import (
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"net"
    26  	"sort"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/coredns/coredns/plugin/etcd/msg"
    31  	"github.com/minio/minio-go/v7/pkg/set"
    32  	clientv3 "go.etcd.io/etcd/client/v3"
    33  )
    34  
    35  // ErrNoEntriesFound - Indicates no entries were found for the given key (directory)
    36  var ErrNoEntriesFound = errors.New("No entries found for this key")
    37  
    38  // ErrDomainMissing - Indicates domain is missing
    39  var ErrDomainMissing = errors.New("domain is missing")
    40  
    41  const etcdPathSeparator = "/"
    42  
    43  // create a new coredns service record for the bucket.
    44  func newCoreDNSMsg(ip string, port string, ttl uint32, t time.Time) ([]byte, error) {
    45  	return json.Marshal(&SrvRecord{
    46  		Host:         ip,
    47  		Port:         json.Number(port),
    48  		TTL:          ttl,
    49  		CreationDate: t,
    50  	})
    51  }
    52  
    53  // Close closes the internal etcd client and cannot be used further
    54  func (c *CoreDNS) Close() error {
    55  	c.etcdClient.Close()
    56  	return nil
    57  }
    58  
    59  // List - Retrieves list of DNS entries for the domain.
    60  func (c *CoreDNS) List() (map[string][]SrvRecord, error) {
    61  	srvRecords := map[string][]SrvRecord{}
    62  	for _, domainName := range c.domainNames {
    63  		key := msg.Path(fmt.Sprintf("%s.", domainName), c.prefixPath)
    64  		records, err := c.list(key+etcdPathSeparator, true)
    65  		if err != nil {
    66  			return srvRecords, err
    67  		}
    68  		for _, record := range records {
    69  			if record.Key == "" {
    70  				continue
    71  			}
    72  			srvRecords[record.Key] = append(srvRecords[record.Key], record)
    73  		}
    74  	}
    75  	return srvRecords, nil
    76  }
    77  
    78  // Get - Retrieves DNS records for a bucket.
    79  func (c *CoreDNS) Get(bucket string) ([]SrvRecord, error) {
    80  	var srvRecords []SrvRecord
    81  	for _, domainName := range c.domainNames {
    82  		key := msg.Path(fmt.Sprintf("%s.%s.", bucket, domainName), c.prefixPath)
    83  		records, err := c.list(key, false)
    84  		if err != nil {
    85  			return nil, err
    86  		}
    87  		// Make sure we have record.Key is empty
    88  		// this can only happen when record.Key
    89  		// has bucket entry with exact prefix
    90  		// match any record.Key which do not
    91  		// match the prefixes we skip them.
    92  		for _, record := range records {
    93  			if record.Key != "" {
    94  				continue
    95  			}
    96  			srvRecords = append(srvRecords, record)
    97  		}
    98  	}
    99  	if len(srvRecords) == 0 {
   100  		return nil, ErrNoEntriesFound
   101  	}
   102  	return srvRecords, nil
   103  }
   104  
   105  // msgUnPath converts a etcd path to domainname.
   106  func msgUnPath(s string) string {
   107  	ks := strings.Split(strings.Trim(s, etcdPathSeparator), etcdPathSeparator)
   108  	for i, j := 0, len(ks)-1; i < j; i, j = i+1, j-1 {
   109  		ks[i], ks[j] = ks[j], ks[i]
   110  	}
   111  	return strings.Join(ks, ".")
   112  }
   113  
   114  // Retrieves list of entries under the key passed.
   115  // Note that this method fetches entries upto only two levels deep.
   116  func (c *CoreDNS) list(key string, domain bool) ([]SrvRecord, error) {
   117  	ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
   118  	r, err := c.etcdClient.Get(ctx, key, clientv3.WithPrefix())
   119  	defer cancel()
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	if r.Count == 0 {
   125  		key = strings.TrimSuffix(key, etcdPathSeparator)
   126  		r, err = c.etcdClient.Get(ctx, key)
   127  		if err != nil {
   128  			return nil, err
   129  		}
   130  		// only if we are looking at `domain` as true
   131  		// we should return error here.
   132  		if domain && r.Count == 0 {
   133  			return nil, ErrDomainMissing
   134  		}
   135  	}
   136  
   137  	var srvRecords []SrvRecord
   138  	for _, n := range r.Kvs {
   139  		var srvRecord SrvRecord
   140  		if err = json.Unmarshal(n.Value, &srvRecord); err != nil {
   141  			return nil, err
   142  		}
   143  		srvRecord.Key = strings.TrimPrefix(string(n.Key), key)
   144  		srvRecord.Key = strings.TrimSuffix(srvRecord.Key, srvRecord.Host)
   145  
   146  		// Skip non-bucket entry like for a key
   147  		// /skydns/net/miniocloud/10.0.0.1 that may exist as
   148  		// dns entry for the server (rather than the bucket
   149  		// itself).
   150  		if srvRecord.Key == "" {
   151  			continue
   152  		}
   153  
   154  		srvRecord.Key = msgUnPath(srvRecord.Key)
   155  		srvRecords = append(srvRecords, srvRecord)
   156  
   157  	}
   158  	sort.Slice(srvRecords, func(i int, j int) bool {
   159  		return srvRecords[i].Key < srvRecords[j].Key
   160  	})
   161  	return srvRecords, nil
   162  }
   163  
   164  // Put - Adds DNS entries into etcd endpoint in CoreDNS etcd message format.
   165  func (c *CoreDNS) Put(bucket string) error {
   166  	c.Delete(bucket) // delete any existing entries.
   167  
   168  	t := time.Now().UTC()
   169  	for ip := range c.domainIPs {
   170  		bucketMsg, err := newCoreDNSMsg(ip, c.domainPort, defaultTTL, t)
   171  		if err != nil {
   172  			return err
   173  		}
   174  		for _, domainName := range c.domainNames {
   175  			key := msg.Path(fmt.Sprintf("%s.%s", bucket, domainName), c.prefixPath)
   176  			key = key + etcdPathSeparator + ip
   177  			ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
   178  			_, err = c.etcdClient.Put(ctx, key, string(bucketMsg))
   179  			cancel()
   180  			if err != nil {
   181  				ctx, cancel = context.WithTimeout(context.Background(), defaultContextTimeout)
   182  				c.etcdClient.Delete(ctx, key)
   183  				cancel()
   184  				return err
   185  			}
   186  		}
   187  	}
   188  	return nil
   189  }
   190  
   191  // Delete - Removes DNS entries added in Put().
   192  func (c *CoreDNS) Delete(bucket string) error {
   193  	for _, domainName := range c.domainNames {
   194  		key := msg.Path(fmt.Sprintf("%s.%s.", bucket, domainName), c.prefixPath)
   195  		ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
   196  		_, err := c.etcdClient.Delete(ctx, key+etcdPathSeparator, clientv3.WithPrefix())
   197  		cancel()
   198  		if err != nil {
   199  			return err
   200  		}
   201  	}
   202  	return nil
   203  }
   204  
   205  // DeleteRecord - Removes a specific DNS entry
   206  func (c *CoreDNS) DeleteRecord(record SrvRecord) error {
   207  	for _, domainName := range c.domainNames {
   208  		key := msg.Path(fmt.Sprintf("%s.%s.", record.Key, domainName), c.prefixPath)
   209  
   210  		ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
   211  		_, err := c.etcdClient.Delete(ctx, key+etcdPathSeparator+record.Host)
   212  		cancel()
   213  		if err != nil {
   214  			return err
   215  		}
   216  	}
   217  	return nil
   218  }
   219  
   220  // String stringer name for this implementation of dns.Store
   221  func (c *CoreDNS) String() string {
   222  	return "etcdDNS"
   223  }
   224  
   225  // CoreDNS - represents dns config for coredns server.
   226  type CoreDNS struct {
   227  	domainNames []string
   228  	domainIPs   set.StringSet
   229  	domainPort  string
   230  	prefixPath  string
   231  	etcdClient  *clientv3.Client
   232  }
   233  
   234  // EtcdOption - functional options pattern style
   235  type EtcdOption func(*CoreDNS)
   236  
   237  // DomainNames set a list of domain names used by this CoreDNS
   238  // client setting, note this will fail if set to empty when
   239  // constructor initializes.
   240  func DomainNames(domainNames []string) EtcdOption {
   241  	return func(args *CoreDNS) {
   242  		args.domainNames = domainNames
   243  	}
   244  }
   245  
   246  // DomainIPs set a list of custom domain IPs, note this will
   247  // fail if set to empty when constructor initializes.
   248  func DomainIPs(domainIPs set.StringSet) EtcdOption {
   249  	return func(args *CoreDNS) {
   250  		args.domainIPs = domainIPs
   251  	}
   252  }
   253  
   254  // DomainPort - is a string version of server port
   255  func DomainPort(domainPort string) EtcdOption {
   256  	return func(args *CoreDNS) {
   257  		args.domainPort = domainPort
   258  	}
   259  }
   260  
   261  // CoreDNSPath - custom prefix on etcd to populate DNS
   262  // service records, optional and can be empty.
   263  // if empty then c.prefixPath is used i.e "/skydns"
   264  func CoreDNSPath(prefix string) EtcdOption {
   265  	return func(args *CoreDNS) {
   266  		args.prefixPath = prefix
   267  	}
   268  }
   269  
   270  // NewCoreDNS - initialize a new coreDNS set/unset values.
   271  func NewCoreDNS(cfg clientv3.Config, setters ...EtcdOption) (Store, error) {
   272  	etcdClient, err := clientv3.New(cfg)
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  
   277  	args := &CoreDNS{
   278  		etcdClient: etcdClient,
   279  	}
   280  
   281  	for _, setter := range setters {
   282  		setter(args)
   283  	}
   284  
   285  	if len(args.domainNames) == 0 || args.domainIPs.IsEmpty() {
   286  		return nil, errors.New("invalid argument")
   287  	}
   288  
   289  	// strip ports off of domainIPs
   290  	domainIPsWithoutPorts := args.domainIPs.ApplyFunc(func(ip string) string {
   291  		host, _, err := net.SplitHostPort(ip)
   292  		if err != nil {
   293  			if strings.Contains(err.Error(), "missing port in address") {
   294  				host = ip
   295  			}
   296  		}
   297  		return host
   298  	})
   299  	args.domainIPs = domainIPsWithoutPorts
   300  
   301  	return args, nil
   302  }