sigs.k8s.io/external-dns@v0.14.1/provider/alibabacloud/alibaba_cloud.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 alibabacloud 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 "sort" 24 "strings" 25 "sync" 26 "time" 27 28 "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" 29 "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" 30 "github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz" 31 "github.com/denverdino/aliyungo/metadata" 32 log "github.com/sirupsen/logrus" 33 "gopkg.in/yaml.v2" 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 defaultAlibabaCloudRecordTTL = 600 42 defaultAlibabaCloudPrivateZoneRecordTTL = 60 43 defaultAlibabaCloudPageSize = 50 44 nullHostAlibabaCloud = "@" 45 pVTZDoamin = "pvtz.aliyuncs.com" 46 defaultAlibabaCloudRequestScheme = "https" 47 ) 48 49 // AlibabaCloudDNSAPI is a minimal implementation of DNS API that we actually use, used primarily for unit testing. 50 // See https://help.aliyun.com/document_detail/29739.html for descriptions of all of its methods. 51 type AlibabaCloudDNSAPI interface { 52 AddDomainRecord(request *alidns.AddDomainRecordRequest) (response *alidns.AddDomainRecordResponse, err error) 53 DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (response *alidns.DeleteDomainRecordResponse, err error) 54 UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (response *alidns.UpdateDomainRecordResponse, err error) 55 DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (response *alidns.DescribeDomainRecordsResponse, err error) 56 DescribeDomains(request *alidns.DescribeDomainsRequest) (response *alidns.DescribeDomainsResponse, err error) 57 } 58 59 // AlibabaCloudPrivateZoneAPI is a minimal implementation of Private Zone API that we actually use, used primarily for unit testing. 60 // See https://help.aliyun.com/document_detail/66234.html for descriptions of all of its methods. 61 type AlibabaCloudPrivateZoneAPI interface { 62 AddZoneRecord(request *pvtz.AddZoneRecordRequest) (response *pvtz.AddZoneRecordResponse, err error) 63 DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (response *pvtz.DeleteZoneRecordResponse, err error) 64 UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (response *pvtz.UpdateZoneRecordResponse, err error) 65 DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (response *pvtz.DescribeZoneRecordsResponse, err error) 66 DescribeZones(request *pvtz.DescribeZonesRequest) (response *pvtz.DescribeZonesResponse, err error) 67 DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (response *pvtz.DescribeZoneInfoResponse, err error) 68 } 69 70 // AlibabaCloudProvider implements the DNS provider for Alibaba Cloud. 71 type AlibabaCloudProvider struct { 72 provider.BaseProvider 73 domainFilter endpoint.DomainFilter 74 zoneIDFilter provider.ZoneIDFilter // Private Zone only 75 MaxChangeCount int 76 EvaluateTargetHealth bool 77 AssumeRole string 78 vpcID string // Private Zone only 79 dryRun bool 80 dnsClient AlibabaCloudDNSAPI 81 pvtzClient AlibabaCloudPrivateZoneAPI 82 privateZone bool 83 clientLock sync.RWMutex 84 nextExpire time.Time 85 } 86 87 type alibabaCloudConfig struct { 88 RegionID string `json:"regionId" yaml:"regionId"` 89 AccessKeyID string `json:"accessKeyId" yaml:"accessKeyId"` 90 AccessKeySecret string `json:"accessKeySecret" yaml:"accessKeySecret"` 91 VPCID string `json:"vpcId" yaml:"vpcId"` 92 RoleName string `json:"-" yaml:"-"` // For ECS RAM role only 93 StsToken string `json:"-" yaml:"-"` 94 ExpireTime time.Time `json:"-" yaml:"-"` 95 } 96 97 // NewAlibabaCloudProvider creates a new Alibaba Cloud provider. 98 // 99 // Returns the provider or an error if a provider could not be created. 100 func NewAlibabaCloudProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFileter provider.ZoneIDFilter, zoneType string, dryRun bool) (*AlibabaCloudProvider, error) { 101 cfg := alibabaCloudConfig{} 102 if configFile != "" { 103 contents, err := os.ReadFile(configFile) 104 if err != nil { 105 return nil, fmt.Errorf("failed to read Alibaba Cloud config file '%s': %v", configFile, err) 106 } 107 err = yaml.Unmarshal(contents, &cfg) 108 if err != nil { 109 return nil, fmt.Errorf("failed to parse Alibaba Cloud config file '%s': %v", configFile, err) 110 } 111 } else { 112 var tmpError error 113 cfg, tmpError = getCloudConfigFromStsToken() 114 if tmpError != nil { 115 return nil, fmt.Errorf("failed to getCloudConfigFromStsToken: %v", tmpError) 116 } 117 } 118 119 // Public DNS service 120 var dnsClient AlibabaCloudDNSAPI 121 var err error 122 123 if cfg.RoleName == "" { 124 dnsClient, err = alidns.NewClientWithAccessKey( 125 cfg.RegionID, 126 cfg.AccessKeyID, 127 cfg.AccessKeySecret, 128 ) 129 } else { 130 dnsClient, err = alidns.NewClientWithStsToken( 131 cfg.RegionID, 132 cfg.AccessKeyID, 133 cfg.AccessKeySecret, 134 cfg.StsToken, 135 ) 136 } 137 138 if err != nil { 139 return nil, fmt.Errorf("failed to create Alibaba Cloud DNS client: %v", err) 140 } 141 142 // Private DNS service 143 var pvtzClient AlibabaCloudPrivateZoneAPI 144 if cfg.RoleName == "" { 145 pvtzClient, err = pvtz.NewClientWithAccessKey( 146 "cn-hangzhou", // The Private Zone location is fixed 147 cfg.AccessKeyID, 148 cfg.AccessKeySecret, 149 ) 150 } else { 151 pvtzClient, err = pvtz.NewClientWithStsToken( 152 cfg.RegionID, 153 cfg.AccessKeyID, 154 cfg.AccessKeySecret, 155 cfg.StsToken, 156 ) 157 } 158 159 if err != nil { 160 return nil, err 161 } 162 163 provider := &AlibabaCloudProvider{ 164 domainFilter: domainFilter, 165 zoneIDFilter: zoneIDFileter, 166 vpcID: cfg.VPCID, 167 dryRun: dryRun, 168 dnsClient: dnsClient, 169 pvtzClient: pvtzClient, 170 privateZone: zoneType == "private", 171 } 172 173 if cfg.RoleName != "" { 174 provider.setNextExpire(cfg.ExpireTime) 175 go provider.refreshStsToken(1 * time.Second) 176 } 177 return provider, nil 178 } 179 180 func getCloudConfigFromStsToken() (alibabaCloudConfig, error) { 181 cfg := alibabaCloudConfig{} 182 // Load config from Metadata Service 183 m := metadata.NewMetaData(nil) 184 roleName := "" 185 var err error 186 if roleName, err = m.RoleName(); err != nil { 187 return cfg, fmt.Errorf("failed to get role name from Metadata Service: %v", err) 188 } 189 vpcID, err := m.VpcID() 190 if err != nil { 191 return cfg, fmt.Errorf("failed to get VPC ID from Metadata Service: %v", err) 192 } 193 regionID, err := m.Region() 194 if err != nil { 195 return cfg, fmt.Errorf("failed to get Region ID from Metadata Service: %v", err) 196 } 197 role, err := m.RamRoleToken(roleName) 198 if err != nil { 199 return cfg, fmt.Errorf("failed to get STS Token from Metadata Service: %v", err) 200 } 201 cfg.RegionID = regionID 202 cfg.RoleName = roleName 203 cfg.VPCID = vpcID 204 cfg.AccessKeyID = role.AccessKeyId 205 cfg.AccessKeySecret = role.AccessKeySecret 206 cfg.StsToken = role.SecurityToken 207 cfg.ExpireTime = role.Expiration 208 return cfg, nil 209 } 210 211 func (p *AlibabaCloudProvider) getDNSClient() AlibabaCloudDNSAPI { 212 p.clientLock.RLock() 213 defer p.clientLock.RUnlock() 214 return p.dnsClient 215 } 216 217 func (p *AlibabaCloudProvider) getPvtzClient() AlibabaCloudPrivateZoneAPI { 218 p.clientLock.RLock() 219 defer p.clientLock.RUnlock() 220 return p.pvtzClient 221 } 222 223 func (p *AlibabaCloudProvider) setNextExpire(expireTime time.Time) { 224 p.clientLock.Lock() 225 defer p.clientLock.Unlock() 226 p.nextExpire = expireTime 227 } 228 229 func (p *AlibabaCloudProvider) refreshStsToken(sleepTime time.Duration) { 230 for { 231 time.Sleep(sleepTime) 232 now := time.Now() 233 utcLocation, err := time.LoadLocation("") 234 if err != nil { 235 log.Errorf("Get utc time error %v", err) 236 continue 237 } 238 nowTime := now.In(utcLocation) 239 p.clientLock.RLock() 240 sleepTime = p.nextExpire.Sub(nowTime) 241 p.clientLock.RUnlock() 242 log.Infof("Distance expiration time %v", sleepTime) 243 if sleepTime < 10*time.Minute { 244 sleepTime = time.Second * 1 245 } else { 246 sleepTime = 9 * time.Minute 247 log.Info("Next fetch sts sleep interval : ", sleepTime.String()) 248 continue 249 } 250 cfg, err := getCloudConfigFromStsToken() 251 if err != nil { 252 log.Errorf("Failed to getCloudConfigFromStsToken: %v", err) 253 continue 254 } 255 dnsClient, err := alidns.NewClientWithStsToken( 256 cfg.RegionID, 257 cfg.AccessKeyID, 258 cfg.AccessKeySecret, 259 cfg.StsToken, 260 ) 261 if err != nil { 262 log.Errorf("Failed to new client with sts token %v", err) 263 continue 264 } 265 pvtzClient, err := pvtz.NewClientWithStsToken( 266 cfg.RegionID, 267 cfg.AccessKeyID, 268 cfg.AccessKeySecret, 269 cfg.StsToken, 270 ) 271 if err != nil { 272 log.Errorf("Failed to new client with sts token %v", err) 273 continue 274 } 275 log.Infof("Refresh client from sts token, next expire time %v", cfg.ExpireTime) 276 p.clientLock.Lock() 277 p.dnsClient = dnsClient 278 p.pvtzClient = pvtzClient 279 p.nextExpire = cfg.ExpireTime 280 p.clientLock.Unlock() 281 } 282 } 283 284 // Records gets the current records. 285 // 286 // Returns the current records or an error if the operation failed. 287 func (p *AlibabaCloudProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) { 288 if p.privateZone { 289 endpoints, err = p.privateZoneRecords() 290 } else { 291 endpoints, err = p.recordsForDNS() 292 } 293 return endpoints, err 294 } 295 296 // ApplyChanges applies the given changes. 297 // 298 // Returns nil if the operation was successful or an error if the operation failed. 299 func (p *AlibabaCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 300 if changes == nil || len(changes.Create)+len(changes.Delete)+len(changes.UpdateNew) == 0 { 301 // No op 302 return nil 303 } 304 305 if p.privateZone { 306 return p.applyChangesForPrivateZone(changes) 307 } 308 return p.applyChangesForDNS(changes) 309 } 310 311 func (p *AlibabaCloudProvider) getDNSName(rr, domain string) string { 312 if rr == nullHostAlibabaCloud { 313 return domain 314 } 315 return rr + "." + domain 316 } 317 318 // recordsForDNS gets the current records. 319 // 320 // Returns the current records or an error if the operation failed. 321 func (p *AlibabaCloudProvider) recordsForDNS() (endpoints []*endpoint.Endpoint, _ error) { 322 records, err := p.records() 323 if err != nil { 324 return nil, err 325 } 326 for _, recordList := range p.groupRecords(records) { 327 name := p.getDNSName(recordList[0].RR, recordList[0].DomainName) 328 recordType := recordList[0].Type 329 ttl := recordList[0].TTL 330 331 var targets []string 332 for _, record := range recordList { 333 target := record.Value 334 if recordType == "TXT" { 335 target = p.unescapeTXTRecordValue(target) 336 } 337 targets = append(targets, target) 338 } 339 ep := endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...) 340 endpoints = append(endpoints, ep) 341 } 342 return endpoints, nil 343 } 344 345 func getNextPageNumber(pageNumber, pageSize, totalCount int64) int64 { 346 if pageNumber*pageSize >= totalCount { 347 return 0 348 } 349 return pageNumber + 1 350 } 351 352 func (p *AlibabaCloudProvider) getRecordKey(record alidns.Record) string { 353 if record.RR == nullHostAlibabaCloud { 354 return record.Type + ":" + record.DomainName 355 } 356 return record.Type + ":" + record.RR + "." + record.DomainName 357 } 358 359 func (p *AlibabaCloudProvider) getRecordKeyByEndpoint(endpoint *endpoint.Endpoint) string { 360 return endpoint.RecordType + ":" + endpoint.DNSName 361 } 362 363 func (p *AlibabaCloudProvider) groupRecords(records []alidns.Record) (endpointMap map[string][]alidns.Record) { 364 endpointMap = make(map[string][]alidns.Record) 365 for _, record := range records { 366 key := p.getRecordKey(record) 367 368 recordList := endpointMap[key] 369 endpointMap[key] = append(recordList, record) 370 } 371 return endpointMap 372 } 373 374 func (p *AlibabaCloudProvider) records() ([]alidns.Record, error) { 375 log.Infof("Retrieving Alibaba Cloud DNS Domain Records") 376 var results []alidns.Record 377 hostedZoneDomains, err := p.getDomainList() 378 if err != nil { 379 return results, fmt.Errorf("getting domain list: %w", err) 380 } 381 if !p.domainFilter.IsConfigured() { 382 for _, zoneDomain := range hostedZoneDomains { 383 domainRecords, err := p.getDomainRecords(zoneDomain) 384 if err != nil { 385 return nil, fmt.Errorf("getDomainRecords %q: %w", zoneDomain, err) 386 } 387 results = append(results, domainRecords...) 388 } 389 } else { 390 for _, domainName := range p.domainFilter.Filters { 391 _, domainName = p.splitDNSName(domainName, hostedZoneDomains) 392 tmpResults, err := p.getDomainRecords(domainName) 393 if err != nil { 394 log.Errorf("getDomainRecords %s error %v", domainName, err) 395 continue 396 } 397 results = append(results, tmpResults...) 398 } 399 } 400 log.Infof("Found %d Alibaba Cloud DNS record(s).", len(results)) 401 return results, nil 402 } 403 404 func (p *AlibabaCloudProvider) getDomainList() ([]string, error) { 405 var domainNames []string 406 request := alidns.CreateDescribeDomainsRequest() 407 request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) 408 request.PageNumber = "1" 409 request.Scheme = defaultAlibabaCloudRequestScheme 410 for { 411 resp, err := p.dnsClient.DescribeDomains(request) 412 if err != nil { 413 log.Errorf("Failed to describe domains for Alibaba Cloud DNS: %v", err) 414 return nil, err 415 } 416 for _, tmpDomain := range resp.Domains.Domain { 417 domainNames = append(domainNames, tmpDomain.DomainName) 418 } 419 nextPage := getNextPageNumber(resp.PageNumber, defaultAlibabaCloudPageSize, resp.TotalCount) 420 if nextPage == 0 { 421 break 422 } else { 423 request.PageNumber = requests.NewInteger64(nextPage) 424 } 425 } 426 return domainNames, nil 427 } 428 429 func (p *AlibabaCloudProvider) getDomainRecords(domainName string) ([]alidns.Record, error) { 430 var results []alidns.Record 431 request := alidns.CreateDescribeDomainRecordsRequest() 432 request.DomainName = domainName 433 request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) 434 request.PageNumber = "1" 435 request.Scheme = defaultAlibabaCloudRequestScheme 436 for { 437 response, err := p.getDNSClient().DescribeDomainRecords(request) 438 if err != nil { 439 log.Errorf("Failed to describe domain records for Alibaba Cloud DNS: %v", err) 440 return nil, err 441 } 442 443 for _, record := range response.DomainRecords.Record { 444 domainName := record.RR + "." + record.DomainName 445 recordType := record.Type 446 447 if !p.domainFilter.Match(domainName) { 448 continue 449 } 450 if !provider.SupportedRecordType(recordType) { 451 continue 452 } 453 // TODO filter Locked record 454 results = append(results, record) 455 } 456 nextPage := getNextPageNumber(response.PageNumber, defaultAlibabaCloudPageSize, response.TotalCount) 457 if nextPage == 0 { 458 break 459 } else { 460 request.PageNumber = requests.NewInteger64(nextPage) 461 } 462 } 463 464 return results, nil 465 } 466 467 func (p *AlibabaCloudProvider) applyChangesForDNS(changes *plan.Changes) error { 468 log.Infof("ApplyChanges to Alibaba Cloud DNS: %++v", *changes) 469 470 records, err := p.records() 471 if err != nil { 472 return err 473 } 474 475 recordMap := p.groupRecords(records) 476 477 hostedZoneDomains, err := p.getDomainList() 478 if err != nil { 479 return fmt.Errorf("getting domain list: %w", err) 480 } 481 482 p.createRecords(changes.Create, hostedZoneDomains) 483 p.deleteRecords(recordMap, changes.Delete) 484 p.updateRecords(recordMap, changes.UpdateNew, hostedZoneDomains) 485 return nil 486 } 487 488 func (p *AlibabaCloudProvider) escapeTXTRecordValue(value string) string { 489 // For unsupported chars 490 return value 491 } 492 493 func (p *AlibabaCloudProvider) unescapeTXTRecordValue(value string) string { 494 if strings.HasPrefix(value, "heritage=") { 495 return fmt.Sprintf("\"%s\"", strings.Replace(value, ";", ",", -1)) 496 } 497 return value 498 } 499 500 func (p *AlibabaCloudProvider) createRecord(endpoint *endpoint.Endpoint, target string, hostedZoneDomains []string) error { 501 rr, domain := p.splitDNSName(endpoint.DNSName, hostedZoneDomains) 502 request := alidns.CreateAddDomainRecordRequest() 503 request.DomainName = domain 504 request.Type = endpoint.RecordType 505 request.RR = rr 506 request.Scheme = defaultAlibabaCloudRequestScheme 507 508 ttl := int(endpoint.RecordTTL) 509 if ttl != 0 { 510 request.TTL = requests.NewInteger(ttl) 511 } 512 513 if endpoint.RecordType == "TXT" { 514 target = p.escapeTXTRecordValue(target) 515 } 516 517 request.Value = target 518 519 if p.dryRun { 520 log.Infof("Dry run: Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud DNS", endpoint.RecordType, endpoint.DNSName, target, ttl) 521 return nil 522 } 523 524 response, err := p.getDNSClient().AddDomainRecord(request) 525 if err == nil { 526 log.Infof("Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud DNS: Record ID=%s", endpoint.RecordType, endpoint.DNSName, target, ttl, response.RecordId) 527 } else { 528 log.Errorf("Failed to create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud DNS: %v", endpoint.RecordType, endpoint.DNSName, target, ttl, err) 529 } 530 return err 531 } 532 533 func (p *AlibabaCloudProvider) createRecords(endpoints []*endpoint.Endpoint, hostedZoneDomains []string) error { 534 for _, endpoint := range endpoints { 535 for _, target := range endpoint.Targets { 536 p.createRecord(endpoint, target, hostedZoneDomains) 537 } 538 } 539 return nil 540 } 541 542 func (p *AlibabaCloudProvider) deleteRecord(recordID string) error { 543 if p.dryRun { 544 log.Infof("Dry run: Delete record id '%s' in Alibaba Cloud DNS", recordID) 545 return nil 546 } 547 548 request := alidns.CreateDeleteDomainRecordRequest() 549 request.RecordId = recordID 550 request.Scheme = defaultAlibabaCloudRequestScheme 551 response, err := p.getDNSClient().DeleteDomainRecord(request) 552 if err == nil { 553 log.Infof("Delete record id %s in Alibaba Cloud DNS", response.RecordId) 554 } else { 555 log.Errorf("Failed to delete record '%s' in Alibaba Cloud DNS: %v", response.RecordId, err) 556 } 557 return err 558 } 559 560 func (p *AlibabaCloudProvider) updateRecord(record alidns.Record, endpoint *endpoint.Endpoint) error { 561 request := alidns.CreateUpdateDomainRecordRequest() 562 request.RecordId = record.RecordId 563 request.RR = record.RR 564 request.Type = record.Type 565 request.Value = record.Value 566 request.Scheme = defaultAlibabaCloudRequestScheme 567 ttl := int(endpoint.RecordTTL) 568 if ttl != 0 { 569 request.TTL = requests.NewInteger(ttl) 570 } 571 response, err := p.getDNSClient().UpdateDomainRecord(request) 572 if err == nil { 573 log.Infof("Update record id '%s' in Alibaba Cloud DNS", response.RecordId) 574 } else { 575 log.Errorf("Failed to update record '%s' in Alibaba Cloud DNS: %v", response.RecordId, err) 576 } 577 return err 578 } 579 580 func (p *AlibabaCloudProvider) deleteRecords(recordMap map[string][]alidns.Record, endpoints []*endpoint.Endpoint) error { 581 for _, endpoint := range endpoints { 582 key := p.getRecordKeyByEndpoint(endpoint) 583 records := recordMap[key] 584 found := false 585 for _, record := range records { 586 value := record.Value 587 if record.Type == "TXT" { 588 value = p.unescapeTXTRecordValue(value) 589 } 590 591 for _, target := range endpoint.Targets { 592 // Find matched record to delete 593 if value == target { 594 p.deleteRecord(record.RecordId) 595 found = true 596 break 597 } 598 } 599 } 600 if !found { 601 log.Errorf("Failed to find %s record named '%s' to delete for Alibaba Cloud DNS", endpoint.RecordType, endpoint.DNSName) 602 } 603 } 604 return nil 605 } 606 607 func (p *AlibabaCloudProvider) equals(record alidns.Record, endpoint *endpoint.Endpoint) bool { 608 ttl1 := record.TTL 609 if ttl1 == defaultAlibabaCloudRecordTTL { 610 ttl1 = 0 611 } 612 613 ttl2 := int64(endpoint.RecordTTL) 614 if ttl2 == defaultAlibabaCloudRecordTTL { 615 ttl2 = 0 616 } 617 618 return ttl1 == ttl2 619 } 620 621 func (p *AlibabaCloudProvider) updateRecords(recordMap map[string][]alidns.Record, endpoints []*endpoint.Endpoint, hostedZoneDomains []string) error { 622 for _, endpoint := range endpoints { 623 key := p.getRecordKeyByEndpoint(endpoint) 624 records := recordMap[key] 625 for _, record := range records { 626 value := record.Value 627 if record.Type == "TXT" { 628 value = p.unescapeTXTRecordValue(value) 629 } 630 found := false 631 for _, target := range endpoint.Targets { 632 // Find matched record to delete 633 if value == target { 634 found = true 635 } 636 } 637 if found { 638 if !p.equals(record, endpoint) { 639 // Update record 640 p.updateRecord(record, endpoint) 641 } 642 } else { 643 p.deleteRecord(record.RecordId) 644 } 645 } 646 for _, target := range endpoint.Targets { 647 if endpoint.RecordType == "TXT" { 648 target = p.escapeTXTRecordValue(target) 649 } 650 found := false 651 for _, record := range records { 652 // Find matched record to delete 653 if record.Value == target { 654 found = true 655 } 656 } 657 if !found { 658 p.createRecord(endpoint, target, hostedZoneDomains) 659 } 660 } 661 } 662 return nil 663 } 664 665 func (p *AlibabaCloudProvider) splitDNSName(dnsName string, hostedZoneDomains []string) (rr string, domain string) { 666 name := strings.TrimSuffix(dnsName, ".") 667 668 // sort zones by dot count; make sure subdomains sort earlier 669 sort.Slice(hostedZoneDomains, func(i, j int) bool { 670 return strings.Count(hostedZoneDomains[i], ".") > strings.Count(hostedZoneDomains[j], ".") 671 }) 672 673 for _, filter := range hostedZoneDomains { 674 if strings.HasSuffix(name, "."+filter) { 675 rr = name[0 : len(name)-len(filter)-1] 676 domain = filter 677 break 678 } else if name == filter { 679 domain = filter 680 rr = "" 681 } 682 } 683 684 if rr == "" { 685 rr = nullHostAlibabaCloud 686 } 687 return rr, domain 688 } 689 690 func (p *AlibabaCloudProvider) matchVPC(zoneID string) bool { 691 request := pvtz.CreateDescribeZoneInfoRequest() 692 request.ZoneId = zoneID 693 request.Domain = pVTZDoamin 694 request.Scheme = defaultAlibabaCloudRequestScheme 695 response, err := p.getPvtzClient().DescribeZoneInfo(request) 696 if err != nil { 697 log.Errorf("Failed to describe zone info %s in Alibaba Cloud DNS: %v", zoneID, err) 698 return false 699 } 700 foundVPC := false 701 for _, vpc := range response.BindVpcs.Vpc { 702 if vpc.VpcId == p.vpcID { 703 foundVPC = true 704 break 705 } 706 } 707 return foundVPC 708 } 709 710 func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) { 711 var zones []pvtz.Zone 712 713 request := pvtz.CreateDescribeZonesRequest() 714 request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) 715 request.PageNumber = "1" 716 request.Domain = pVTZDoamin 717 request.Scheme = defaultAlibabaCloudRequestScheme 718 for { 719 response, err := p.getPvtzClient().DescribeZones(request) 720 if err != nil { 721 log.Errorf("Failed to describe zones in Alibaba Cloud DNS: %v", err) 722 return nil, err 723 } 724 for _, zone := range response.Zones.Zone { 725 log.Infof("PrivateZones zone: %++v", zone) 726 727 if !p.zoneIDFilter.Match(zone.ZoneId) { 728 continue 729 } 730 if !p.domainFilter.Match(zone.ZoneName) { 731 continue 732 } 733 if !p.matchVPC(zone.ZoneId) { 734 continue 735 } 736 zones = append(zones, zone) 737 } 738 nextPage := getNextPageNumber(int64(response.PageNumber), defaultAlibabaCloudPageSize, int64(response.TotalItems)) 739 if nextPage == 0 { 740 break 741 } else { 742 request.PageNumber = requests.NewInteger64(nextPage) 743 } 744 } 745 return zones, nil 746 } 747 748 type alibabaPrivateZone struct { 749 pvtz.Zone 750 records []pvtz.Record 751 } 752 753 func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone, error) { 754 log.Infof("Retrieving Alibaba Cloud Private Zone records") 755 756 result := make(map[string]*alibabaPrivateZone) 757 recordsCount := 0 758 759 zones, err := p.privateZones() 760 if err != nil { 761 return nil, err 762 } 763 764 for _, zone := range zones { 765 request := pvtz.CreateDescribeZoneRecordsRequest() 766 request.ZoneId = zone.ZoneId 767 request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) 768 request.PageNumber = "1" 769 request.Domain = pVTZDoamin 770 request.Scheme = defaultAlibabaCloudRequestScheme 771 var records []pvtz.Record 772 773 for { 774 response, err := p.getPvtzClient().DescribeZoneRecords(request) 775 if err != nil { 776 log.Errorf("Failed to describe zone record '%s' in Alibaba Cloud DNS: %v", zone.ZoneId, err) 777 return nil, err 778 } 779 780 for _, record := range response.Records.Record { 781 recordType := record.Type 782 783 if !provider.SupportedRecordType(recordType) { 784 continue 785 } 786 787 // TODO filter Locked 788 records = append(records, record) 789 } 790 nextPage := getNextPageNumber(int64(response.PageNumber), defaultAlibabaCloudPageSize, int64(response.TotalItems)) 791 if nextPage == 0 { 792 break 793 } else { 794 request.PageNumber = requests.NewInteger64(nextPage) 795 } 796 } 797 798 privateZone := alibabaPrivateZone{ 799 Zone: zone, 800 records: records, 801 } 802 recordsCount += len(records) 803 result[zone.ZoneName] = &privateZone 804 } 805 log.Infof("Found %d Alibaba Cloud Private Zone record(s).", recordsCount) 806 return result, nil 807 } 808 809 func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) (endpointMap map[string][]pvtz.Record) { 810 endpointMap = make(map[string][]pvtz.Record) 811 812 for _, record := range zone.records { 813 key := record.Type + ":" + record.Rr 814 recordList := endpointMap[key] 815 endpointMap[key] = append(recordList, record) 816 } 817 818 return endpointMap 819 } 820 821 // recordsForPrivateZone gets the current records. 822 // 823 // Returns the current records or an error if the operation failed. 824 func (p *AlibabaCloudProvider) privateZoneRecords() (endpoints []*endpoint.Endpoint, _ error) { 825 zones, err := p.getPrivateZones() 826 if err != nil { 827 return nil, err 828 } 829 830 for _, zone := range zones { 831 recordMap := p.groupPrivateZoneRecords(zone) 832 for _, recordList := range recordMap { 833 name := p.getDNSName(recordList[0].Rr, zone.ZoneName) 834 recordType := recordList[0].Type 835 ttl := recordList[0].Ttl 836 if ttl == defaultAlibabaCloudPrivateZoneRecordTTL { 837 ttl = 0 838 } 839 var targets []string 840 for _, record := range recordList { 841 target := record.Value 842 if recordType == "TXT" { 843 target = p.unescapeTXTRecordValue(target) 844 } 845 targets = append(targets, target) 846 } 847 ep := endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...) 848 endpoints = append(endpoints, ep) 849 } 850 } 851 return endpoints, nil 852 } 853 854 func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibabaPrivateZone, endpoint *endpoint.Endpoint, target string) error { 855 rr, domain := p.splitDNSName(endpoint.DNSName, keys(zones)) 856 zone := zones[domain] 857 if zone == nil { 858 err := fmt.Errorf("failed to find private zone '%s'", domain) 859 log.Errorf("Failed to create %s record named '%s' to '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, target, err) 860 return err 861 } 862 863 request := pvtz.CreateAddZoneRecordRequest() 864 request.ZoneId = zone.ZoneId 865 request.Type = endpoint.RecordType 866 request.Rr = rr 867 request.Domain = pVTZDoamin 868 request.Scheme = defaultAlibabaCloudRequestScheme 869 870 ttl := int(endpoint.RecordTTL) 871 if ttl != 0 { 872 request.Ttl = requests.NewInteger(ttl) 873 } 874 875 if endpoint.RecordType == "TXT" { 876 target = p.escapeTXTRecordValue(target) 877 } 878 879 request.Value = target 880 881 if p.dryRun { 882 log.Infof("Dry run: Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud Private Zone", endpoint.RecordType, endpoint.DNSName, target, ttl) 883 return nil 884 } 885 886 response, err := p.getPvtzClient().AddZoneRecord(request) 887 if err == nil { 888 log.Infof("Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud Private Zone: Record ID=%d", endpoint.RecordType, endpoint.DNSName, target, ttl, response.RecordId) 889 } else { 890 log.Errorf("Failed to create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, target, ttl, err) 891 } 892 return err 893 } 894 895 func (p *AlibabaCloudProvider) createPrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error { 896 for _, endpoint := range endpoints { 897 for _, target := range endpoint.Targets { 898 p.createPrivateZoneRecord(zones, endpoint, target) 899 } 900 } 901 return nil 902 } 903 904 func (p *AlibabaCloudProvider) deletePrivateZoneRecord(recordID int64) error { 905 if p.dryRun { 906 log.Infof("Dry run: Delete record id '%d' in Alibaba Cloud Private Zone", recordID) 907 } 908 909 request := pvtz.CreateDeleteZoneRecordRequest() 910 request.RecordId = requests.NewInteger64(recordID) 911 request.Domain = pVTZDoamin 912 request.Scheme = defaultAlibabaCloudRequestScheme 913 914 response, err := p.getPvtzClient().DeleteZoneRecord(request) 915 if err == nil { 916 log.Infof("Delete record id '%d' in Alibaba Cloud Private Zone", response.RecordId) 917 } else { 918 log.Errorf("Failed to delete record %d in Alibaba Cloud Private Zone: %v", response.RecordId, err) 919 } 920 return err 921 } 922 923 func (p *AlibabaCloudProvider) deletePrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error { 924 zoneNames := keys(zones) 925 for _, endpoint := range endpoints { 926 rr, domain := p.splitDNSName(endpoint.DNSName, zoneNames) 927 928 zone := zones[domain] 929 if zone == nil { 930 err := fmt.Errorf("failed to find private zone '%s'", domain) 931 log.Errorf("Failed to delete %s record named '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, err) 932 continue 933 } 934 found := false 935 for _, record := range zone.records { 936 if rr == record.Rr && endpoint.RecordType == record.Type { 937 value := record.Value 938 if record.Type == "TXT" { 939 value = p.unescapeTXTRecordValue(value) 940 } 941 for _, target := range endpoint.Targets { 942 // Find matched record to delete 943 if value == target { 944 p.deletePrivateZoneRecord(record.RecordId) 945 found = true 946 break 947 } 948 } 949 } 950 } 951 if !found { 952 log.Errorf("Failed to find %s record named '%s' to delete for Alibaba Cloud Private Zone", endpoint.RecordType, endpoint.DNSName) 953 } 954 } 955 return nil 956 } 957 958 // ApplyChanges applies the given changes. 959 // 960 // Returns nil if the operation was successful or an error if the operation failed. 961 func (p *AlibabaCloudProvider) applyChangesForPrivateZone(changes *plan.Changes) error { 962 log.Infof("ApplyChanges to Alibaba Cloud Private Zone: %++v", *changes) 963 964 zones, err := p.getPrivateZones() 965 if err != nil { 966 return err 967 } 968 969 for zoneName, zone := range zones { 970 log.Debugf("%s: %++v", zoneName, zone) 971 } 972 973 p.createPrivateZoneRecords(zones, changes.Create) 974 p.deletePrivateZoneRecords(zones, changes.Delete) 975 p.updatePrivateZoneRecords(zones, changes.UpdateNew) 976 return nil 977 } 978 979 func (p *AlibabaCloudProvider) updatePrivateZoneRecord(record pvtz.Record, endpoint *endpoint.Endpoint) error { 980 request := pvtz.CreateUpdateZoneRecordRequest() 981 request.RecordId = requests.NewInteger64(record.RecordId) 982 request.Rr = record.Rr 983 request.Type = record.Type 984 request.Value = record.Value 985 request.Domain = pVTZDoamin 986 request.Scheme = defaultAlibabaCloudRequestScheme 987 ttl := int(endpoint.RecordTTL) 988 if ttl != 0 { 989 request.Ttl = requests.NewInteger(ttl) 990 } 991 response, err := p.getPvtzClient().UpdateZoneRecord(request) 992 if err == nil { 993 log.Infof("Update record id '%d' in Alibaba Cloud Private Zone", response.RecordId) 994 } else { 995 log.Errorf("Failed to update record '%d' in Alibaba Cloud Private Zone: %v", response.RecordId, err) 996 } 997 return err 998 } 999 1000 func (p *AlibabaCloudProvider) equalsPrivateZone(record pvtz.Record, endpoint *endpoint.Endpoint) bool { 1001 ttl1 := record.Ttl 1002 if ttl1 == defaultAlibabaCloudPrivateZoneRecordTTL { 1003 ttl1 = 0 1004 } 1005 1006 ttl2 := int(endpoint.RecordTTL) 1007 if ttl2 == defaultAlibabaCloudPrivateZoneRecordTTL { 1008 ttl2 = 0 1009 } 1010 1011 return ttl1 == ttl2 1012 } 1013 1014 func (p *AlibabaCloudProvider) updatePrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error { 1015 zoneNames := keys(zones) 1016 for _, endpoint := range endpoints { 1017 rr, domain := p.splitDNSName(endpoint.DNSName, zoneNames) 1018 zone := zones[domain] 1019 if zone == nil { 1020 err := fmt.Errorf("failed to find private zone '%s'", domain) 1021 log.Errorf("Failed to update %s record named '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, err) 1022 continue 1023 } 1024 1025 for _, record := range zone.records { 1026 if record.Rr != rr || record.Type != endpoint.RecordType { 1027 continue 1028 } 1029 value := record.Value 1030 if record.Type == "TXT" { 1031 value = p.unescapeTXTRecordValue(value) 1032 } 1033 found := false 1034 for _, target := range endpoint.Targets { 1035 // Find matched record to delete 1036 if value == target { 1037 found = true 1038 break 1039 } 1040 } 1041 if found { 1042 if !p.equalsPrivateZone(record, endpoint) { 1043 // Update record 1044 p.updatePrivateZoneRecord(record, endpoint) 1045 } 1046 } else { 1047 p.deletePrivateZoneRecord(record.RecordId) 1048 } 1049 } 1050 for _, target := range endpoint.Targets { 1051 if endpoint.RecordType == "TXT" { 1052 target = p.escapeTXTRecordValue(target) 1053 } 1054 found := false 1055 for _, record := range zone.records { 1056 if record.Rr != rr || record.Type != endpoint.RecordType { 1057 continue 1058 } 1059 // Find matched record to delete 1060 if record.Value == target { 1061 found = true 1062 break 1063 } 1064 } 1065 if !found { 1066 p.createPrivateZoneRecord(zones, endpoint, target) 1067 } 1068 } 1069 } 1070 return nil 1071 } 1072 1073 func keys[T any](value map[string]T) []string { 1074 var results []string 1075 for k := range value { 1076 results = append(results, k) 1077 } 1078 return results 1079 }