sigs.k8s.io/external-dns@v0.14.1/provider/pdns/pdns.go (about) 1 /* 2 Copyright 2018 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 pdns 18 19 import ( 20 "bytes" 21 "context" 22 "crypto/tls" 23 "encoding/json" 24 "errors" 25 "math" 26 "net" 27 "net/http" 28 "sort" 29 "strings" 30 "time" 31 32 pgo "github.com/ffledgling/pdns-go" 33 log "github.com/sirupsen/logrus" 34 35 "sigs.k8s.io/external-dns/endpoint" 36 "sigs.k8s.io/external-dns/pkg/tlsutils" 37 "sigs.k8s.io/external-dns/plan" 38 "sigs.k8s.io/external-dns/provider" 39 ) 40 41 type pdnsChangeType string 42 43 const ( 44 apiBase = "/api/v1" 45 46 // Unless we use something like pdnsproxy (discontinued upstream), this value will _always_ be localhost 47 defaultServerID = "localhost" 48 defaultTTL = 300 49 50 // PdnsDelete and PdnsReplace are effectively an enum for "pgo.RrSet.changetype" 51 // TODO: Can we somehow get this from the pgo swagger client library itself? 52 53 // PdnsDelete : PowerDNS changetype used for deleting rrsets 54 // ref: https://doc.powerdns.com/authoritative/http-api/zone.html#rrset (see "changetype") 55 PdnsDelete pdnsChangeType = "DELETE" 56 // PdnsReplace : PowerDNS changetype for creating, updating and patching rrsets 57 PdnsReplace pdnsChangeType = "REPLACE" 58 // Number of times to retry failed PDNS requests 59 retryLimit = 3 60 // time in milliseconds 61 retryAfterTime = 250 * time.Millisecond 62 ) 63 64 // PDNSConfig is comprised of the fields necessary to create a new PDNSProvider 65 type PDNSConfig struct { 66 DomainFilter endpoint.DomainFilter 67 DryRun bool 68 Server string 69 APIKey string 70 TLSConfig TLSConfig 71 } 72 73 // TLSConfig is comprised of the TLS-related fields necessary to create a new PDNSProvider 74 type TLSConfig struct { 75 SkipTLSVerify bool 76 CAFilePath string 77 ClientCertFilePath string 78 ClientCertKeyFilePath string 79 } 80 81 func (tlsConfig *TLSConfig) setHTTPClient(pdnsClientConfig *pgo.Configuration) error { 82 log.Debug("Configuring TLS for PDNS Provider.") 83 tlsClientConfig, err := tlsutils.NewTLSConfig( 84 tlsConfig.ClientCertFilePath, 85 tlsConfig.ClientCertKeyFilePath, 86 tlsConfig.CAFilePath, 87 "", 88 tlsConfig.SkipTLSVerify, 89 tls.VersionTLS12, 90 ) 91 if err != nil { 92 return err 93 } 94 95 // Timeouts taken from net.http.DefaultTransport 96 transporter := &http.Transport{ 97 Proxy: http.ProxyFromEnvironment, 98 DialContext: (&net.Dialer{ 99 Timeout: 30 * time.Second, 100 KeepAlive: 30 * time.Second, 101 DualStack: true, 102 }).DialContext, 103 MaxIdleConns: 100, 104 IdleConnTimeout: 90 * time.Second, 105 TLSHandshakeTimeout: 10 * time.Second, 106 ExpectContinueTimeout: 1 * time.Second, 107 TLSClientConfig: tlsClientConfig, 108 } 109 pdnsClientConfig.HTTPClient = &http.Client{ 110 Transport: transporter, 111 } 112 113 return nil 114 } 115 116 // Function for debug printing 117 func stringifyHTTPResponseBody(r *http.Response) (body string) { 118 if r == nil { 119 return "" 120 } 121 122 buf := new(bytes.Buffer) 123 buf.ReadFrom(r.Body) 124 body = buf.String() 125 return body 126 } 127 128 // PDNSAPIProvider : Interface used and extended by the PDNSAPIClient struct as 129 // well as mock APIClients used in testing 130 type PDNSAPIProvider interface { 131 ListZones() ([]pgo.Zone, *http.Response, error) 132 PartitionZones(zones []pgo.Zone) ([]pgo.Zone, []pgo.Zone) 133 ListZone(zoneID string) (pgo.Zone, *http.Response, error) 134 PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) 135 } 136 137 // PDNSAPIClient : Struct that encapsulates all the PowerDNS specific implementation details 138 type PDNSAPIClient struct { 139 dryRun bool 140 authCtx context.Context 141 client *pgo.APIClient 142 domainFilter endpoint.DomainFilter 143 } 144 145 // ListZones : Method returns all enabled zones from PowerDNS 146 // ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones 147 func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err error) { 148 for i := 0; i < retryLimit; i++ { 149 zones, resp, err = c.client.ZonesApi.ListZones(c.authCtx, defaultServerID) 150 if err != nil { 151 log.Debugf("Unable to fetch zones %v", err) 152 log.Debugf("Retrying ListZones() ... %d", i) 153 time.Sleep(retryAfterTime * (1 << uint(i))) 154 continue 155 } 156 return zones, resp, err 157 } 158 159 log.Errorf("Unable to fetch zones. %v", err) 160 return zones, resp, err 161 } 162 163 // PartitionZones : Method returns a slice of zones that adhere to the domain filter and a slice of ones that does not adhere to the filter 164 func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) (filteredZones []pgo.Zone, residualZones []pgo.Zone) { 165 if c.domainFilter.IsConfigured() { 166 for _, zone := range zones { 167 if c.domainFilter.Match(zone.Name) { 168 filteredZones = append(filteredZones, zone) 169 } else { 170 residualZones = append(residualZones, zone) 171 } 172 } 173 } else { 174 filteredZones = zones 175 } 176 return filteredZones, residualZones 177 } 178 179 // ListZone : Method returns the details of a specific zone from PowerDNS 180 // ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones-zone_id 181 func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Response, err error) { 182 for i := 0; i < retryLimit; i++ { 183 zone, resp, err = c.client.ZonesApi.ListZone(c.authCtx, defaultServerID, zoneID) 184 if err != nil { 185 log.Debugf("Unable to fetch zone %v", err) 186 log.Debugf("Retrying ListZone() ... %d", i) 187 time.Sleep(retryAfterTime * (1 << uint(i))) 188 continue 189 } 190 return zone, resp, err 191 } 192 193 log.Errorf("Unable to list zone. %v", err) 194 return zone, resp, err 195 } 196 197 // PatchZone : Method used to update the contents of a particular zone from PowerDNS 198 // ref: https://doc.powerdns.com/authoritative/http-api/zone.html#patch--servers-server_id-zones-zone_id 199 func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *http.Response, err error) { 200 for i := 0; i < retryLimit; i++ { 201 resp, err = c.client.ZonesApi.PatchZone(c.authCtx, defaultServerID, zoneID, zoneStruct) 202 if err != nil { 203 log.Debugf("Unable to patch zone %v", err) 204 log.Debugf("Retrying PatchZone() ... %d", i) 205 time.Sleep(retryAfterTime * (1 << uint(i))) 206 continue 207 } 208 return resp, err 209 } 210 211 log.Errorf("Unable to patch zone. %v", err) 212 return resp, err 213 } 214 215 // PDNSProvider is an implementation of the Provider interface for PowerDNS 216 type PDNSProvider struct { 217 provider.BaseProvider 218 client PDNSAPIProvider 219 } 220 221 // NewPDNSProvider initializes a new PowerDNS based Provider. 222 func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, error) { 223 // Do some input validation 224 225 if config.APIKey == "" { 226 return nil, errors.New("missing API Key for PDNS. Specify using --pdns-api-key=") 227 } 228 229 // We do not support dry running, exit safely instead of surprising the user 230 // TODO: Add Dry Run support 231 if config.DryRun { 232 return nil, errors.New("PDNS Provider does not currently support dry-run") 233 } 234 235 if config.Server == "localhost" { 236 log.Warnf("PDNS Server is set to localhost, this may not be what you want. Specify using --pdns-server=") 237 } 238 239 pdnsClientConfig := pgo.NewConfiguration() 240 pdnsClientConfig.BasePath = config.Server + apiBase 241 if err := config.TLSConfig.setHTTPClient(pdnsClientConfig); err != nil { 242 return nil, err 243 } 244 245 provider := &PDNSProvider{ 246 client: &PDNSAPIClient{ 247 dryRun: config.DryRun, 248 authCtx: context.WithValue(ctx, pgo.ContextAPIKey, pgo.APIKey{Key: config.APIKey}), 249 client: pgo.NewAPIClient(pdnsClientConfig), 250 domainFilter: config.DomainFilter, 251 }, 252 } 253 return provider, nil 254 } 255 256 func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error) { 257 endpoints = []*endpoint.Endpoint{} 258 targets := []string{} 259 rrType_ := rr.Type_ 260 261 for _, record := range rr.Records { 262 // If a record is "Disabled", it's not supposed to be "visible" 263 if !record.Disabled { 264 targets = append(targets, record.Content) 265 } 266 } 267 if rr.Type_ == "ALIAS" { 268 rrType_ = "CNAME" 269 } 270 endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rrType_, endpoint.TTL(rr.Ttl), targets...)) 271 return endpoints, nil 272 } 273 274 // ConvertEndpointsToZones marshals endpoints into pdns compatible Zone structs 275 func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error) { 276 zonelist = []pgo.Zone{} 277 endpoints := make([]*endpoint.Endpoint, len(eps)) 278 copy(endpoints, eps) 279 280 // Sort the endpoints array so we have deterministic inserts 281 sort.SliceStable(endpoints, 282 func(i, j int) bool { 283 // We only care about sorting endpoints with the same dnsname 284 if endpoints[i].DNSName == endpoints[j].DNSName { 285 return endpoints[i].RecordType < endpoints[j].RecordType 286 } 287 return endpoints[i].DNSName < endpoints[j].DNSName 288 }) 289 290 zones, _, err := p.client.ListZones() 291 if err != nil { 292 return nil, err 293 } 294 filteredZones, residualZones := p.client.PartitionZones(zones) 295 296 // Sort the zone by length of the name in descending order, we use this 297 // property later to ensure we add a record to the longest matching zone 298 299 sort.SliceStable(filteredZones, func(i, j int) bool { return len(filteredZones[i].Name) > len(filteredZones[j].Name) }) 300 301 // NOTE: Complexity of this loop is O(FilteredZones*Endpoints). 302 // A possibly faster implementation would be a search of the reversed 303 // DNSName in a trie of Zone names, which should be O(Endpoints), but at this point it's not 304 // necessary. 305 for _, zone := range filteredZones { 306 zone.Rrsets = []pgo.RrSet{} 307 for i := 0; i < len(endpoints); { 308 ep := endpoints[i] 309 dnsname := provider.EnsureTrailingDot(ep.DNSName) 310 if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) { 311 // The assumption here is that there will only ever be one target 312 // per (ep.DNSName, ep.RecordType) tuple, which holds true for 313 // external-dns v5.0.0-alpha onwards 314 records := []pgo.Record{} 315 RecordType_ := ep.RecordType 316 for _, t := range ep.Targets { 317 if ep.RecordType == "CNAME" || ep.RecordType == "ALIAS" { 318 t = provider.EnsureTrailingDot(t) 319 } 320 records = append(records, pgo.Record{Content: t}) 321 } 322 323 if dnsname == zone.Name && ep.RecordType == "CNAME" { 324 log.Debugf("Converting APEX record %s from CNAME to ALIAS", dnsname) 325 RecordType_ = "ALIAS" 326 } 327 328 rrset := pgo.RrSet{ 329 Name: dnsname, 330 Type_: RecordType_, 331 Records: records, 332 Changetype: string(changetype), 333 } 334 335 // DELETEs explicitly forbid a TTL, therefore only PATCHes need the TTL 336 if changetype == PdnsReplace { 337 if int64(ep.RecordTTL) > int64(math.MaxInt32) { 338 return nil, errors.New("value of record TTL overflows, limited to int32") 339 } 340 if ep.RecordTTL == 0 { 341 // No TTL was specified for the record, we use the default 342 rrset.Ttl = int32(defaultTTL) 343 } else { 344 rrset.Ttl = int32(ep.RecordTTL) 345 } 346 } 347 348 zone.Rrsets = append(zone.Rrsets, rrset) 349 350 // "pop" endpoint if it's matched 351 endpoints = append(endpoints[0:i], endpoints[i+1:]...) 352 } else { 353 // If we didn't pop anything, we move to the next item in the list 354 i++ 355 } 356 } 357 if len(zone.Rrsets) > 0 { 358 zonelist = append(zonelist, zone) 359 } 360 } 361 362 // residualZones is unsorted by name length like its counterpart 363 // since we only care to remove endpoints that do not match domain filter 364 for _, zone := range residualZones { 365 for i := 0; i < len(endpoints); { 366 ep := endpoints[i] 367 dnsname := provider.EnsureTrailingDot(ep.DNSName) 368 if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) { 369 // "pop" endpoint if it's matched to a residual zone... essentially a no-op 370 log.Debugf("Ignoring Endpoint because it was matched to a zone that was not specified within Domain Filter(s): %s", dnsname) 371 endpoints = append(endpoints[0:i], endpoints[i+1:]...) 372 } else { 373 i++ 374 } 375 } 376 } 377 // If we still have some endpoints left, it means we couldn't find a matching zone (filtered or residual) for them 378 // We warn instead of hard fail here because we don't want a misconfig to cause everything to go down 379 if len(endpoints) > 0 { 380 log.Warnf("No matching zones were found for the following endpoints: %+v", endpoints) 381 } 382 383 log.Debugf("Zone List generated from Endpoints: %+v", zonelist) 384 385 return zonelist, nil 386 } 387 388 // mutateRecords takes a list of endpoints and creates, replaces or deletes them based on the changetype 389 func (p *PDNSProvider) mutateRecords(endpoints []*endpoint.Endpoint, changetype pdnsChangeType) error { 390 zonelist, err := p.ConvertEndpointsToZones(endpoints, changetype) 391 if err != nil { 392 return err 393 } 394 for _, zone := range zonelist { 395 jso, err := json.Marshal(zone) 396 if err != nil { 397 log.Errorf("JSON Marshal for zone struct failed!") 398 } else { 399 log.Debugf("Struct for PatchZone:\n%s", string(jso)) 400 } 401 resp, err := p.client.PatchZone(zone.Id, zone) 402 if err != nil { 403 log.Debugf("PDNS API response: %s", stringifyHTTPResponseBody(resp)) 404 return err 405 } 406 } 407 return nil 408 } 409 410 // Records returns all DNS records controlled by the configured PDNS server (for all zones) 411 func (p *PDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { 412 zones, _, err := p.client.ListZones() 413 if err != nil { 414 return nil, err 415 } 416 filteredZones, _ := p.client.PartitionZones(zones) 417 418 for _, zone := range filteredZones { 419 z, _, err := p.client.ListZone(zone.Id) 420 if err != nil { 421 log.Warnf("Unable to fetch Records") 422 return nil, err 423 } 424 425 for _, rr := range z.Rrsets { 426 e, err := p.convertRRSetToEndpoints(rr) 427 if err != nil { 428 return nil, err 429 } 430 endpoints = append(endpoints, e...) 431 } 432 } 433 434 log.Debugf("Records fetched:\n%+v", endpoints) 435 return endpoints, nil 436 } 437 438 // ApplyChanges takes a list of changes (endpoints) and updates the PDNS server 439 // by sending the correct HTTP PATCH requests to a matching zone 440 func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 441 startTime := time.Now() 442 443 // Create 444 for _, change := range changes.Create { 445 log.Infof("CREATE: %+v", change) 446 } 447 // We only attempt to mutate records if there are any to mutate. A 448 // call to mutate records with an empty list of endpoints is still a 449 // valid call and a no-op, but we might as well not make the call to 450 // prevent unnecessary logging 451 if len(changes.Create) > 0 { 452 // "Replacing" non-existent records creates them 453 err := p.mutateRecords(changes.Create, PdnsReplace) 454 if err != nil { 455 return err 456 } 457 } 458 459 // Update 460 for _, change := range changes.UpdateOld { 461 // Since PDNS "Patches", we don't need to specify the "old" 462 // record. The Update New change type will automatically take 463 // care of replacing the old RRSet with the new one We simply 464 // leave this logging here for information 465 log.Debugf("UPDATE-OLD (ignored): %+v", change) 466 } 467 468 for _, change := range changes.UpdateNew { 469 log.Infof("UPDATE-NEW: %+v", change) 470 } 471 if len(changes.UpdateNew) > 0 { 472 err := p.mutateRecords(changes.UpdateNew, PdnsReplace) 473 if err != nil { 474 return err 475 } 476 } 477 478 // Delete 479 for _, change := range changes.Delete { 480 log.Infof("DELETE: %+v", change) 481 } 482 if len(changes.Delete) > 0 { 483 err := p.mutateRecords(changes.Delete, PdnsDelete) 484 if err != nil { 485 return err 486 } 487 } 488 log.Infof("Changes pushed out to PowerDNS in %s\n", time.Since(startTime)) 489 return nil 490 }