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  }