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 }