mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-05 17:16:59 +02:00
1097 lines
32 KiB
Go
1097 lines
32 KiB
Go
/*
|
|
Copyright 2017 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package alibabacloud
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
|
|
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
|
|
"github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz"
|
|
"github.com/denverdino/aliyungo/metadata"
|
|
"github.com/goccy/go-yaml"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"sigs.k8s.io/external-dns/endpoint"
|
|
"sigs.k8s.io/external-dns/plan"
|
|
"sigs.k8s.io/external-dns/provider"
|
|
)
|
|
|
|
const (
|
|
defaultTTL = 600
|
|
defaultAlibabaCloudPrivateZoneRecordTTL = 60
|
|
defaultAlibabaCloudPageSize = 50
|
|
nullHostAlibabaCloud = "@"
|
|
pVTZDoamin = "pvtz.aliyuncs.com"
|
|
defaultAlibabaCloudRequestScheme = "https"
|
|
)
|
|
|
|
// AlibabaCloudDNSAPI is a minimal implementation of DNS API that we actually use, used primarily for unit testing.
|
|
// See https://help.aliyun.com/document_detail/29739.html for descriptions of all of its methods.
|
|
type AlibabaCloudDNSAPI interface {
|
|
AddDomainRecord(request *alidns.AddDomainRecordRequest) (*alidns.AddDomainRecordResponse, error)
|
|
DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (*alidns.DeleteDomainRecordResponse, error)
|
|
UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (*alidns.UpdateDomainRecordResponse, error)
|
|
DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (*alidns.DescribeDomainRecordsResponse, error)
|
|
DescribeDomains(request *alidns.DescribeDomainsRequest) (*alidns.DescribeDomainsResponse, error)
|
|
}
|
|
|
|
// AlibabaCloudPrivateZoneAPI is a minimal implementation of Private Zone API that we actually use, used primarily for unit testing.
|
|
// See https://help.aliyun.com/document_detail/66234.html for descriptions of all of its methods.
|
|
type AlibabaCloudPrivateZoneAPI interface {
|
|
AddZoneRecord(request *pvtz.AddZoneRecordRequest) (*pvtz.AddZoneRecordResponse, error)
|
|
DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (*pvtz.DeleteZoneRecordResponse, error)
|
|
UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (*pvtz.UpdateZoneRecordResponse, error)
|
|
DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (*pvtz.DescribeZoneRecordsResponse, error)
|
|
DescribeZones(request *pvtz.DescribeZonesRequest) (*pvtz.DescribeZonesResponse, error)
|
|
DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (*pvtz.DescribeZoneInfoResponse, error)
|
|
}
|
|
|
|
// AlibabaCloudProvider implements the DNS provider for Alibaba Cloud.
|
|
type AlibabaCloudProvider struct {
|
|
provider.BaseProvider
|
|
domainFilter *endpoint.DomainFilter
|
|
zoneIDFilter provider.ZoneIDFilter // Private Zone only
|
|
MaxChangeCount int
|
|
EvaluateTargetHealth bool
|
|
AssumeRole string
|
|
vpcID string // Private Zone only
|
|
dryRun bool
|
|
dnsClient AlibabaCloudDNSAPI
|
|
pvtzClient AlibabaCloudPrivateZoneAPI
|
|
privateZone bool
|
|
clientLock sync.RWMutex
|
|
nextExpire time.Time
|
|
}
|
|
|
|
type alibabaCloudConfig struct {
|
|
RegionID string `json:"regionId" yaml:"regionId"`
|
|
AccessKeyID string `json:"accessKeyId" yaml:"accessKeyId"`
|
|
AccessKeySecret string `json:"accessKeySecret" yaml:"accessKeySecret"`
|
|
VPCID string `json:"vpcId" yaml:"vpcId"`
|
|
RoleName string `json:"-" yaml:"-"` // For ECS RAM role only
|
|
StsToken string `json:"-" yaml:"-"`
|
|
ExpireTime time.Time `json:"-" yaml:"-"`
|
|
}
|
|
|
|
// NewAlibabaCloudProvider creates a new Alibaba Cloud provider.
|
|
//
|
|
// Returns the provider or an error if a provider could not be created.
|
|
func NewAlibabaCloudProvider(configFile string, domainFilter *endpoint.DomainFilter, zoneIDFileter provider.ZoneIDFilter, zoneType string, dryRun bool) (*AlibabaCloudProvider, error) {
|
|
cfg := alibabaCloudConfig{}
|
|
if configFile != "" {
|
|
contents, err := os.ReadFile(configFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read Alibaba Cloud config file '%s': %w", configFile, err)
|
|
}
|
|
err = yaml.Unmarshal(contents, &cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse Alibaba Cloud config file '%s': %w", configFile, err)
|
|
}
|
|
} else {
|
|
var tmpError error
|
|
cfg, tmpError = getCloudConfigFromStsToken()
|
|
if tmpError != nil {
|
|
return nil, fmt.Errorf("failed to getCloudConfigFromStsToken: %w", tmpError)
|
|
}
|
|
}
|
|
|
|
// Public DNS service
|
|
var dnsClient AlibabaCloudDNSAPI
|
|
var err error
|
|
|
|
if cfg.RoleName == "" {
|
|
dnsClient, err = alidns.NewClientWithAccessKey(
|
|
cfg.RegionID,
|
|
cfg.AccessKeyID,
|
|
cfg.AccessKeySecret,
|
|
)
|
|
} else {
|
|
dnsClient, err = alidns.NewClientWithStsToken(
|
|
cfg.RegionID,
|
|
cfg.AccessKeyID,
|
|
cfg.AccessKeySecret,
|
|
cfg.StsToken,
|
|
)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Alibaba Cloud DNS client: %w", err)
|
|
}
|
|
|
|
// Private DNS service
|
|
var pvtzClient AlibabaCloudPrivateZoneAPI
|
|
if cfg.RoleName == "" {
|
|
pvtzClient, err = pvtz.NewClientWithAccessKey(
|
|
"cn-hangzhou", // The Private Zone location is fixed
|
|
cfg.AccessKeyID,
|
|
cfg.AccessKeySecret,
|
|
)
|
|
} else {
|
|
pvtzClient, err = pvtz.NewClientWithStsToken(
|
|
cfg.RegionID,
|
|
cfg.AccessKeyID,
|
|
cfg.AccessKeySecret,
|
|
cfg.StsToken,
|
|
)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
provider := &AlibabaCloudProvider{
|
|
domainFilter: domainFilter,
|
|
zoneIDFilter: zoneIDFileter,
|
|
vpcID: cfg.VPCID,
|
|
dryRun: dryRun,
|
|
dnsClient: dnsClient,
|
|
pvtzClient: pvtzClient,
|
|
privateZone: zoneType == "private",
|
|
}
|
|
|
|
if cfg.RoleName != "" {
|
|
provider.setNextExpire(cfg.ExpireTime)
|
|
go provider.refreshStsToken(1 * time.Second)
|
|
}
|
|
return provider, nil
|
|
}
|
|
|
|
func getCloudConfigFromStsToken() (alibabaCloudConfig, error) {
|
|
cfg := alibabaCloudConfig{}
|
|
// Load config from Metadata Service
|
|
m := metadata.NewMetaData(nil)
|
|
roleName := ""
|
|
var err error
|
|
if roleName, err = m.RoleName(); err != nil {
|
|
return cfg, fmt.Errorf("failed to get role name from Metadata Service: %w", err)
|
|
}
|
|
vpcID, err := m.VpcID()
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("failed to get VPC ID from Metadata Service: %w", err)
|
|
}
|
|
regionID, err := m.Region()
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("failed to get Region ID from Metadata Service: %w", err)
|
|
}
|
|
role, err := m.RamRoleToken(roleName)
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("failed to get STS Token from Metadata Service: %w", err)
|
|
}
|
|
cfg.RegionID = regionID
|
|
cfg.RoleName = roleName
|
|
cfg.VPCID = vpcID
|
|
cfg.AccessKeyID = role.AccessKeyId
|
|
cfg.AccessKeySecret = role.AccessKeySecret
|
|
cfg.StsToken = role.SecurityToken
|
|
cfg.ExpireTime = role.Expiration
|
|
return cfg, nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) getDNSClient() AlibabaCloudDNSAPI {
|
|
p.clientLock.RLock()
|
|
defer p.clientLock.RUnlock()
|
|
return p.dnsClient
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) getPvtzClient() AlibabaCloudPrivateZoneAPI {
|
|
p.clientLock.RLock()
|
|
defer p.clientLock.RUnlock()
|
|
return p.pvtzClient
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) setNextExpire(expireTime time.Time) {
|
|
p.clientLock.Lock()
|
|
defer p.clientLock.Unlock()
|
|
p.nextExpire = expireTime
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) refreshStsToken(sleepTime time.Duration) {
|
|
for {
|
|
time.Sleep(sleepTime)
|
|
now := time.Now()
|
|
utcLocation, err := time.LoadLocation("")
|
|
if err != nil {
|
|
log.Errorf("Get utc time error %v", err)
|
|
continue
|
|
}
|
|
nowTime := now.In(utcLocation)
|
|
p.clientLock.RLock()
|
|
sleepTime = p.nextExpire.Sub(nowTime)
|
|
p.clientLock.RUnlock()
|
|
log.Infof("Distance expiration time %v", sleepTime)
|
|
if sleepTime < 10*time.Minute {
|
|
sleepTime = time.Second * 1
|
|
} else {
|
|
sleepTime = 9 * time.Minute
|
|
log.Info("Next fetch sts sleep interval : ", sleepTime.String())
|
|
continue
|
|
}
|
|
cfg, err := getCloudConfigFromStsToken()
|
|
if err != nil {
|
|
log.Errorf("Failed to getCloudConfigFromStsToken: %v", err)
|
|
continue
|
|
}
|
|
dnsClient, err := alidns.NewClientWithStsToken(
|
|
cfg.RegionID,
|
|
cfg.AccessKeyID,
|
|
cfg.AccessKeySecret,
|
|
cfg.StsToken,
|
|
)
|
|
if err != nil {
|
|
log.Errorf("Failed to new client with sts token %v", err)
|
|
continue
|
|
}
|
|
pvtzClient, err := pvtz.NewClientWithStsToken(
|
|
cfg.RegionID,
|
|
cfg.AccessKeyID,
|
|
cfg.AccessKeySecret,
|
|
cfg.StsToken,
|
|
)
|
|
if err != nil {
|
|
log.Errorf("Failed to new client with sts token %v", err)
|
|
continue
|
|
}
|
|
log.Infof("Refresh client from sts token, next expire time %v", cfg.ExpireTime)
|
|
p.clientLock.Lock()
|
|
p.dnsClient = dnsClient
|
|
p.pvtzClient = pvtzClient
|
|
p.nextExpire = cfg.ExpireTime
|
|
p.clientLock.Unlock()
|
|
}
|
|
}
|
|
|
|
// Records gets the current records.
|
|
//
|
|
// Returns the current records or an error if the operation failed.
|
|
func (p *AlibabaCloudProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
|
if p.privateZone {
|
|
return p.privateZoneRecords()
|
|
} else {
|
|
return p.recordsForDNS()
|
|
}
|
|
}
|
|
|
|
// ApplyChanges applies the given changes.
|
|
//
|
|
// Returns nil if the operation was successful or an error if the operation failed.
|
|
func (p *AlibabaCloudProvider) ApplyChanges(_ context.Context, changes *plan.Changes) error {
|
|
if changes == nil || len(changes.Create)+len(changes.Delete)+len(changes.Update) == 0 {
|
|
// No op
|
|
return nil
|
|
}
|
|
|
|
if p.privateZone {
|
|
return p.applyChangesForPrivateZone(changes)
|
|
}
|
|
return p.applyChangesForDNS(changes)
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) getDNSName(rr, domain string) string {
|
|
if rr == nullHostAlibabaCloud {
|
|
return domain
|
|
}
|
|
return rr + "." + domain
|
|
}
|
|
|
|
// recordsForDNS gets the current records.
|
|
//
|
|
// Returns the current records or an error if the operation failed.
|
|
func (p *AlibabaCloudProvider) recordsForDNS() ([]*endpoint.Endpoint, error) {
|
|
records, err := p.records()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
endpoints := make([]*endpoint.Endpoint, 0, len(records))
|
|
for _, recordList := range p.groupRecords(records) {
|
|
name := p.getDNSName(recordList[0].RR, recordList[0].DomainName)
|
|
recordType := recordList[0].Type
|
|
ttl := recordList[0].TTL
|
|
|
|
var targets []string
|
|
for _, record := range recordList {
|
|
target := record.Value
|
|
if recordType == "TXT" {
|
|
target = p.unescapeTXTRecordValue(target)
|
|
}
|
|
targets = append(targets, target)
|
|
}
|
|
ep := endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...)
|
|
endpoints = append(endpoints, ep)
|
|
}
|
|
return endpoints, nil
|
|
}
|
|
|
|
func getNextPageNumber(pageNumber, pageSize, totalCount int64) int64 {
|
|
if pageNumber*pageSize >= totalCount {
|
|
return 0
|
|
}
|
|
return pageNumber + 1
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) getRecordKey(record alidns.Record) string {
|
|
if record.RR == nullHostAlibabaCloud {
|
|
return record.Type + ":" + record.DomainName
|
|
}
|
|
return record.Type + ":" + record.RR + "." + record.DomainName
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) getRecordKeyByEndpoint(endpoint *endpoint.Endpoint) string {
|
|
return endpoint.RecordType + ":" + endpoint.DNSName
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) groupRecords(records []alidns.Record) map[string][]alidns.Record {
|
|
endpointMap := make(map[string][]alidns.Record)
|
|
for _, record := range records {
|
|
key := p.getRecordKey(record)
|
|
|
|
recordList := endpointMap[key]
|
|
endpointMap[key] = append(recordList, record)
|
|
}
|
|
return endpointMap
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) records() ([]alidns.Record, error) {
|
|
log.Infof("Retrieving Alibaba Cloud DNS Domain Records")
|
|
var results []alidns.Record
|
|
hostedZoneDomains, err := p.getDomainList()
|
|
if err != nil {
|
|
return results, fmt.Errorf("getting domain list: %w", err)
|
|
}
|
|
if !p.domainFilter.IsConfigured() {
|
|
for _, zoneDomain := range hostedZoneDomains {
|
|
domainRecords, err := p.getDomainRecords(zoneDomain)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getDomainRecords %q: %w", zoneDomain, err)
|
|
}
|
|
results = append(results, domainRecords...)
|
|
}
|
|
} else {
|
|
for _, domainName := range p.domainFilter.Filters {
|
|
_, domainName = p.splitDNSName(domainName, hostedZoneDomains)
|
|
tmpResults, err := p.getDomainRecords(domainName)
|
|
if err != nil {
|
|
log.Errorf("getDomainRecords %s error %v", domainName, err)
|
|
continue
|
|
}
|
|
results = append(results, tmpResults...)
|
|
}
|
|
}
|
|
log.Infof("Found %d Alibaba Cloud DNS record(s).", len(results))
|
|
return results, nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) getDomainList() ([]string, error) {
|
|
var domainNames []string
|
|
request := alidns.CreateDescribeDomainsRequest()
|
|
request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
|
|
request.PageNumber = "1"
|
|
request.Scheme = defaultAlibabaCloudRequestScheme
|
|
for {
|
|
resp, err := p.dnsClient.DescribeDomains(request)
|
|
if err != nil {
|
|
log.Errorf("Failed to describe domains for Alibaba Cloud DNS: %v", err)
|
|
return nil, err
|
|
}
|
|
for _, tmpDomain := range resp.Domains.Domain {
|
|
domainNames = append(domainNames, tmpDomain.DomainName)
|
|
}
|
|
nextPage := getNextPageNumber(resp.PageNumber, defaultAlibabaCloudPageSize, resp.TotalCount)
|
|
if nextPage == 0 {
|
|
break
|
|
} else {
|
|
request.PageNumber = requests.NewInteger64(nextPage)
|
|
}
|
|
}
|
|
return domainNames, nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) getDomainRecords(domainName string) ([]alidns.Record, error) {
|
|
var results []alidns.Record
|
|
request := alidns.CreateDescribeDomainRecordsRequest()
|
|
request.DomainName = domainName
|
|
request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
|
|
request.PageNumber = "1"
|
|
request.Scheme = defaultAlibabaCloudRequestScheme
|
|
for {
|
|
response, err := p.getDNSClient().DescribeDomainRecords(request)
|
|
if err != nil {
|
|
log.Errorf("Failed to describe domain records for Alibaba Cloud DNS: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
for _, record := range response.DomainRecords.Record {
|
|
domainName := record.RR + "." + record.DomainName
|
|
recordType := record.Type
|
|
|
|
if !p.domainFilter.Match(domainName) {
|
|
continue
|
|
}
|
|
if !provider.SupportedRecordType(recordType) {
|
|
continue
|
|
}
|
|
// TODO filter Locked record
|
|
results = append(results, record)
|
|
}
|
|
nextPage := getNextPageNumber(response.PageNumber, defaultAlibabaCloudPageSize, response.TotalCount)
|
|
if nextPage == 0 {
|
|
break
|
|
} else {
|
|
request.PageNumber = requests.NewInteger64(nextPage)
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) applyChangesForDNS(changes *plan.Changes) error {
|
|
log.Infof("ApplyChanges to Alibaba Cloud DNS: %++v", *changes)
|
|
|
|
records, err := p.records()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
recordMap := p.groupRecords(records)
|
|
|
|
hostedZoneDomains, err := p.getDomainList()
|
|
if err != nil {
|
|
return fmt.Errorf("getting domain list: %w", err)
|
|
}
|
|
|
|
p.createRecords(changes.Create, hostedZoneDomains)
|
|
p.deleteRecords(recordMap, changes.Delete)
|
|
p.updateRecords(recordMap, changes.UpdateNew(), hostedZoneDomains)
|
|
return nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) escapeTXTRecordValue(value string) string {
|
|
// For unsupported chars
|
|
return value
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) unescapeTXTRecordValue(value string) string {
|
|
if strings.HasPrefix(value, "heritage=") {
|
|
return fmt.Sprintf("\"%s\"", strings.ReplaceAll(value, ";", ","))
|
|
}
|
|
return value
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) createRecord(endpoint *endpoint.Endpoint, target string, hostedZoneDomains []string) error {
|
|
if len(hostedZoneDomains) == 0 {
|
|
log.Errorf("Failed to create %s record named '%s' to '%s' for Alibaba Cloud DNS: zone not found",
|
|
endpoint.RecordType, endpoint.DNSName, target)
|
|
return fmt.Errorf("zone not found")
|
|
}
|
|
|
|
rr, domain := p.splitDNSName(endpoint.DNSName, hostedZoneDomains)
|
|
|
|
if domain == "" {
|
|
log.Errorf("Failed to create %s record named '%s' to '%s' for Alibaba Cloud DNS: no corresponding DNS zone found for this domain '%s'",
|
|
endpoint.RecordType, endpoint.DNSName, target, endpoint.DNSName)
|
|
return fmt.Errorf("no corresponding DNS zone found for this domain")
|
|
}
|
|
|
|
request := alidns.CreateAddDomainRecordRequest()
|
|
request.DomainName = domain
|
|
request.Type = endpoint.RecordType
|
|
request.RR = rr
|
|
request.Scheme = defaultAlibabaCloudRequestScheme
|
|
|
|
ttl := int(endpoint.RecordTTL)
|
|
if ttl != 0 {
|
|
request.TTL = requests.NewInteger(ttl)
|
|
}
|
|
|
|
if endpoint.RecordType == "TXT" {
|
|
target = p.escapeTXTRecordValue(target)
|
|
}
|
|
|
|
request.Value = target
|
|
|
|
if p.dryRun {
|
|
log.Infof("Dry run: Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud DNS", endpoint.RecordType, endpoint.DNSName, target, ttl)
|
|
return nil
|
|
}
|
|
|
|
response, err := p.getDNSClient().AddDomainRecord(request)
|
|
if err == nil {
|
|
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)
|
|
} else {
|
|
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)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) createRecords(endpoints []*endpoint.Endpoint, hostedZoneDomains []string) error {
|
|
for _, endpoint := range endpoints {
|
|
for _, target := range endpoint.Targets {
|
|
p.createRecord(endpoint, target, hostedZoneDomains)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) deleteRecord(recordID string) error {
|
|
if p.dryRun {
|
|
log.Infof("Dry run: Delete record id '%s' in Alibaba Cloud DNS", recordID)
|
|
return nil
|
|
}
|
|
|
|
request := alidns.CreateDeleteDomainRecordRequest()
|
|
request.RecordId = recordID
|
|
request.Scheme = defaultAlibabaCloudRequestScheme
|
|
response, err := p.getDNSClient().DeleteDomainRecord(request)
|
|
if err == nil {
|
|
log.Infof("Delete record id %s in Alibaba Cloud DNS", response.RecordId)
|
|
} else {
|
|
log.Errorf("Failed to delete record '%s' in Alibaba Cloud DNS: %v", response.RecordId, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) updateRecord(record alidns.Record, endpoint *endpoint.Endpoint) error {
|
|
request := alidns.CreateUpdateDomainRecordRequest()
|
|
request.RecordId = record.RecordId
|
|
request.RR = record.RR
|
|
request.Type = record.Type
|
|
request.Value = record.Value
|
|
request.Scheme = defaultAlibabaCloudRequestScheme
|
|
ttl := int(endpoint.RecordTTL)
|
|
if ttl != 0 {
|
|
request.TTL = requests.NewInteger(ttl)
|
|
}
|
|
response, err := p.getDNSClient().UpdateDomainRecord(request)
|
|
if err == nil {
|
|
log.Infof("Update record id '%s' in Alibaba Cloud DNS", response.RecordId)
|
|
} else {
|
|
log.Errorf("Failed to update record '%s' in Alibaba Cloud DNS: %v", response.RecordId, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) deleteRecords(recordMap map[string][]alidns.Record, endpoints []*endpoint.Endpoint) error {
|
|
for _, endpoint := range endpoints {
|
|
key := p.getRecordKeyByEndpoint(endpoint)
|
|
records := recordMap[key]
|
|
found := false
|
|
for _, record := range records {
|
|
value := record.Value
|
|
if record.Type == "TXT" {
|
|
value = p.unescapeTXTRecordValue(value)
|
|
}
|
|
|
|
for _, target := range endpoint.Targets {
|
|
// Find matched record to delete
|
|
if value == target {
|
|
p.deleteRecord(record.RecordId)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
log.Errorf("Failed to find %s record named '%s' to delete for Alibaba Cloud DNS", endpoint.RecordType, endpoint.DNSName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) equals(record alidns.Record, endpoint *endpoint.Endpoint) bool {
|
|
ttl1 := record.TTL
|
|
if ttl1 == defaultTTL {
|
|
ttl1 = 0
|
|
}
|
|
|
|
ttl2 := int64(endpoint.RecordTTL)
|
|
if ttl2 == defaultTTL {
|
|
ttl2 = 0
|
|
}
|
|
|
|
return ttl1 == ttl2
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) updateRecords(recordMap map[string][]alidns.Record, endpoints []*endpoint.Endpoint, hostedZoneDomains []string) error {
|
|
for _, endpoint := range endpoints {
|
|
key := p.getRecordKeyByEndpoint(endpoint)
|
|
records := recordMap[key]
|
|
for _, record := range records {
|
|
value := record.Value
|
|
if record.Type == "TXT" {
|
|
value = p.unescapeTXTRecordValue(value)
|
|
}
|
|
found := false
|
|
for _, target := range endpoint.Targets {
|
|
// Find matched record to delete
|
|
if value == target {
|
|
found = true
|
|
}
|
|
}
|
|
if found {
|
|
if !p.equals(record, endpoint) {
|
|
// Update record
|
|
p.updateRecord(record, endpoint)
|
|
}
|
|
} else {
|
|
p.deleteRecord(record.RecordId)
|
|
}
|
|
}
|
|
for _, target := range endpoint.Targets {
|
|
if endpoint.RecordType == "TXT" {
|
|
target = p.escapeTXTRecordValue(target)
|
|
}
|
|
found := false
|
|
for _, record := range records {
|
|
// Find matched record to delete
|
|
if record.Value == target {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
p.createRecord(endpoint, target, hostedZoneDomains)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) splitDNSName(dnsName string, hostedZoneDomains []string) (string, string) {
|
|
name := strings.TrimSuffix(dnsName, ".")
|
|
|
|
// sort zones by dot count; make sure subdomains sort earlier
|
|
sort.Slice(hostedZoneDomains, func(i, j int) bool {
|
|
return strings.Count(hostedZoneDomains[i], ".") > strings.Count(hostedZoneDomains[j], ".")
|
|
})
|
|
|
|
var rr, domain string
|
|
|
|
for _, filter := range hostedZoneDomains {
|
|
if strings.HasSuffix(name, "."+filter) {
|
|
rr = name[0 : len(name)-len(filter)-1]
|
|
domain = filter
|
|
break
|
|
} else if name == filter {
|
|
domain = filter
|
|
rr = ""
|
|
}
|
|
}
|
|
|
|
if rr == "" {
|
|
rr = nullHostAlibabaCloud
|
|
}
|
|
return rr, domain
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) matchVPC(zoneID string) bool {
|
|
request := pvtz.CreateDescribeZoneInfoRequest()
|
|
request.ZoneId = zoneID
|
|
request.Domain = pVTZDoamin
|
|
request.Scheme = defaultAlibabaCloudRequestScheme
|
|
response, err := p.getPvtzClient().DescribeZoneInfo(request)
|
|
if err != nil {
|
|
log.Errorf("Failed to describe zone info %s in Alibaba Cloud DNS: %v", zoneID, err)
|
|
return false
|
|
}
|
|
foundVPC := false
|
|
for _, vpc := range response.BindVpcs.Vpc {
|
|
if vpc.VpcId == p.vpcID {
|
|
foundVPC = true
|
|
break
|
|
}
|
|
}
|
|
return foundVPC
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) {
|
|
var zones []pvtz.Zone
|
|
|
|
request := pvtz.CreateDescribeZonesRequest()
|
|
request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
|
|
request.PageNumber = "1"
|
|
request.Domain = pVTZDoamin
|
|
request.Scheme = defaultAlibabaCloudRequestScheme
|
|
for {
|
|
response, err := p.getPvtzClient().DescribeZones(request)
|
|
if err != nil {
|
|
log.Errorf("Failed to describe zones in Alibaba Cloud DNS: %v", err)
|
|
return nil, err
|
|
}
|
|
for _, zone := range response.Zones.Zone {
|
|
log.Infof("PrivateZones zone: %++v", zone)
|
|
|
|
if !p.zoneIDFilter.Match(zone.ZoneId) {
|
|
continue
|
|
}
|
|
if !p.domainFilter.Match(zone.ZoneName) {
|
|
continue
|
|
}
|
|
if !p.matchVPC(zone.ZoneId) {
|
|
continue
|
|
}
|
|
zones = append(zones, zone)
|
|
}
|
|
nextPage := getNextPageNumber(int64(response.PageNumber), defaultAlibabaCloudPageSize, int64(response.TotalItems))
|
|
if nextPage == 0 {
|
|
break
|
|
} else {
|
|
request.PageNumber = requests.NewInteger64(nextPage)
|
|
}
|
|
}
|
|
return zones, nil
|
|
}
|
|
|
|
type alibabaPrivateZone struct {
|
|
pvtz.Zone
|
|
records []pvtz.Record
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone, error) {
|
|
log.Infof("Retrieving Alibaba Cloud Private Zone records")
|
|
|
|
result := make(map[string]*alibabaPrivateZone)
|
|
recordsCount := 0
|
|
|
|
zones, err := p.privateZones()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, zone := range zones {
|
|
request := pvtz.CreateDescribeZoneRecordsRequest()
|
|
request.ZoneId = zone.ZoneId
|
|
request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
|
|
request.PageNumber = "1"
|
|
request.Domain = pVTZDoamin
|
|
request.Scheme = defaultAlibabaCloudRequestScheme
|
|
var records []pvtz.Record
|
|
|
|
for {
|
|
response, err := p.getPvtzClient().DescribeZoneRecords(request)
|
|
if err != nil {
|
|
log.Errorf("Failed to describe zone record '%s' in Alibaba Cloud DNS: %v", zone.ZoneId, err)
|
|
return nil, err
|
|
}
|
|
|
|
for _, record := range response.Records.Record {
|
|
recordType := record.Type
|
|
|
|
if !provider.SupportedRecordType(recordType) {
|
|
continue
|
|
}
|
|
|
|
// TODO filter Locked
|
|
records = append(records, record)
|
|
}
|
|
nextPage := getNextPageNumber(int64(response.PageNumber), defaultAlibabaCloudPageSize, int64(response.TotalItems))
|
|
if nextPage == 0 {
|
|
break
|
|
} else {
|
|
request.PageNumber = requests.NewInteger64(nextPage)
|
|
}
|
|
}
|
|
|
|
privateZone := alibabaPrivateZone{
|
|
Zone: zone,
|
|
records: records,
|
|
}
|
|
recordsCount += len(records)
|
|
result[zone.ZoneName] = &privateZone
|
|
}
|
|
log.Infof("Found %d Alibaba Cloud Private Zone record(s).", recordsCount)
|
|
return result, nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) map[string][]pvtz.Record {
|
|
endpointMap := make(map[string][]pvtz.Record)
|
|
|
|
for _, record := range zone.records {
|
|
key := record.Type + ":" + record.Rr
|
|
recordList := endpointMap[key]
|
|
endpointMap[key] = append(recordList, record)
|
|
}
|
|
|
|
return endpointMap
|
|
}
|
|
|
|
// recordsForPrivateZone gets the current records.
|
|
//
|
|
// Returns the current records or an error if the operation failed.
|
|
func (p *AlibabaCloudProvider) privateZoneRecords() ([]*endpoint.Endpoint, error) {
|
|
zones, err := p.getPrivateZones()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
endpoints := make([]*endpoint.Endpoint, 0)
|
|
|
|
for _, zone := range zones {
|
|
recordMap := p.groupPrivateZoneRecords(zone)
|
|
for _, recordList := range recordMap {
|
|
name := p.getDNSName(recordList[0].Rr, zone.ZoneName)
|
|
recordType := recordList[0].Type
|
|
ttl := recordList[0].Ttl
|
|
if ttl == defaultAlibabaCloudPrivateZoneRecordTTL {
|
|
ttl = 0
|
|
}
|
|
var targets []string
|
|
for _, record := range recordList {
|
|
target := record.Value
|
|
if recordType == "TXT" {
|
|
target = p.unescapeTXTRecordValue(target)
|
|
}
|
|
targets = append(targets, target)
|
|
}
|
|
ep := endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...)
|
|
endpoints = append(endpoints, ep)
|
|
}
|
|
}
|
|
return endpoints, nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibabaPrivateZone, endpoint *endpoint.Endpoint, target string) error {
|
|
rr, domain := p.splitDNSName(endpoint.DNSName, keys(zones))
|
|
zone := zones[domain]
|
|
if zone == nil {
|
|
err := fmt.Errorf("failed to find private zone '%s'", domain)
|
|
log.Errorf("Failed to create %s record named '%s' to '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, target, err)
|
|
return err
|
|
}
|
|
|
|
request := pvtz.CreateAddZoneRecordRequest()
|
|
request.ZoneId = zone.ZoneId
|
|
request.Type = endpoint.RecordType
|
|
request.Rr = rr
|
|
request.Domain = pVTZDoamin
|
|
request.Scheme = defaultAlibabaCloudRequestScheme
|
|
|
|
ttl := int(endpoint.RecordTTL)
|
|
if ttl != 0 {
|
|
request.Ttl = requests.NewInteger(ttl)
|
|
}
|
|
|
|
if endpoint.RecordType == "TXT" {
|
|
target = p.escapeTXTRecordValue(target)
|
|
}
|
|
|
|
request.Value = target
|
|
|
|
if p.dryRun {
|
|
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)
|
|
return nil
|
|
}
|
|
|
|
response, err := p.getPvtzClient().AddZoneRecord(request)
|
|
if err == nil {
|
|
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)
|
|
} else {
|
|
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)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) createPrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error {
|
|
for _, endpoint := range endpoints {
|
|
for _, target := range endpoint.Targets {
|
|
_ = p.createPrivateZoneRecord(zones, endpoint, target)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) deletePrivateZoneRecord(recordID int64) error {
|
|
if p.dryRun {
|
|
log.Infof("Dry run: Delete record id '%d' in Alibaba Cloud Private Zone", recordID)
|
|
}
|
|
|
|
request := pvtz.CreateDeleteZoneRecordRequest()
|
|
request.RecordId = requests.NewInteger64(recordID)
|
|
request.Domain = pVTZDoamin
|
|
request.Scheme = defaultAlibabaCloudRequestScheme
|
|
|
|
response, err := p.getPvtzClient().DeleteZoneRecord(request)
|
|
if err == nil {
|
|
log.Infof("Delete record id '%d' in Alibaba Cloud Private Zone", response.RecordId)
|
|
} else {
|
|
log.Errorf("Failed to delete record %d in Alibaba Cloud Private Zone: %v", response.RecordId, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) deletePrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error {
|
|
zoneNames := keys(zones)
|
|
for _, endpoint := range endpoints {
|
|
rr, domain := p.splitDNSName(endpoint.DNSName, zoneNames)
|
|
|
|
zone := zones[domain]
|
|
if zone == nil {
|
|
err := fmt.Errorf("failed to find private zone '%s'", domain)
|
|
log.Errorf("Failed to delete %s record named '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, err)
|
|
continue
|
|
}
|
|
found := false
|
|
for _, record := range zone.records {
|
|
if rr == record.Rr && endpoint.RecordType == record.Type {
|
|
value := record.Value
|
|
if record.Type == "TXT" {
|
|
value = p.unescapeTXTRecordValue(value)
|
|
}
|
|
for _, target := range endpoint.Targets {
|
|
// Find matched record to delete
|
|
if value == target {
|
|
p.deletePrivateZoneRecord(record.RecordId)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
log.Errorf("Failed to find %s record named '%s' to delete for Alibaba Cloud Private Zone", endpoint.RecordType, endpoint.DNSName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ApplyChanges applies the given changes.
|
|
//
|
|
// Returns nil if the operation was successful or an error if the operation failed.
|
|
func (p *AlibabaCloudProvider) applyChangesForPrivateZone(changes *plan.Changes) error {
|
|
log.Infof("ApplyChanges to Alibaba Cloud Private Zone: %++v", *changes)
|
|
|
|
zones, err := p.getPrivateZones()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for zoneName, zone := range zones {
|
|
log.Debugf("%s: %++v", zoneName, zone)
|
|
}
|
|
|
|
p.createPrivateZoneRecords(zones, changes.Create)
|
|
p.deletePrivateZoneRecords(zones, changes.Delete)
|
|
p.updatePrivateZoneRecords(zones, changes.UpdateNew())
|
|
return nil
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) updatePrivateZoneRecord(record pvtz.Record, endpoint *endpoint.Endpoint) error {
|
|
request := pvtz.CreateUpdateZoneRecordRequest()
|
|
request.RecordId = requests.NewInteger64(record.RecordId)
|
|
request.Rr = record.Rr
|
|
request.Type = record.Type
|
|
request.Value = record.Value
|
|
request.Domain = pVTZDoamin
|
|
request.Scheme = defaultAlibabaCloudRequestScheme
|
|
ttl := int(endpoint.RecordTTL)
|
|
if ttl != 0 {
|
|
request.Ttl = requests.NewInteger(ttl)
|
|
}
|
|
response, err := p.getPvtzClient().UpdateZoneRecord(request)
|
|
if err == nil {
|
|
log.Infof("Update record id '%d' in Alibaba Cloud Private Zone", response.RecordId)
|
|
} else {
|
|
log.Errorf("Failed to update record '%d' in Alibaba Cloud Private Zone: %v", response.RecordId, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) equalsPrivateZone(record pvtz.Record, endpoint *endpoint.Endpoint) bool {
|
|
ttl1 := record.Ttl
|
|
if ttl1 == defaultAlibabaCloudPrivateZoneRecordTTL {
|
|
ttl1 = 0
|
|
}
|
|
|
|
ttl2 := int(endpoint.RecordTTL)
|
|
if ttl2 == defaultAlibabaCloudPrivateZoneRecordTTL {
|
|
ttl2 = 0
|
|
}
|
|
|
|
return ttl1 == ttl2
|
|
}
|
|
|
|
func (p *AlibabaCloudProvider) updatePrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error {
|
|
zoneNames := keys(zones)
|
|
for _, endpoint := range endpoints {
|
|
rr, domain := p.splitDNSName(endpoint.DNSName, zoneNames)
|
|
zone := zones[domain]
|
|
if zone == nil {
|
|
err := fmt.Errorf("failed to find private zone '%s'", domain)
|
|
log.Errorf("Failed to update %s record named '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, err)
|
|
continue
|
|
}
|
|
|
|
for _, record := range zone.records {
|
|
if record.Rr != rr || record.Type != endpoint.RecordType {
|
|
continue
|
|
}
|
|
value := record.Value
|
|
if record.Type == "TXT" {
|
|
value = p.unescapeTXTRecordValue(value)
|
|
}
|
|
found := false
|
|
for _, target := range endpoint.Targets {
|
|
// Find matched record to delete
|
|
if value == target {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
if !p.equalsPrivateZone(record, endpoint) {
|
|
// Update record
|
|
p.updatePrivateZoneRecord(record, endpoint)
|
|
}
|
|
} else {
|
|
p.deletePrivateZoneRecord(record.RecordId)
|
|
}
|
|
}
|
|
for _, target := range endpoint.Targets {
|
|
if endpoint.RecordType == "TXT" {
|
|
target = p.escapeTXTRecordValue(target)
|
|
}
|
|
found := false
|
|
for _, record := range zone.records {
|
|
if record.Rr != rr || record.Type != endpoint.RecordType {
|
|
continue
|
|
}
|
|
// Find matched record to delete
|
|
if record.Value == target {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
p.createPrivateZoneRecord(zones, endpoint, target)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func keys[T any](value map[string]T) []string {
|
|
var results []string
|
|
for k := range value {
|
|
results = append(results, k)
|
|
}
|
|
return results
|
|
}
|