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 }