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 }