Allow usage of (multiple) AWS profiles using .credentials file

This commit is contained in:
Jan Roehrich 2023-10-05 13:29:14 +02:00
parent d2890b0a71
commit 7fb25f44ce
No known key found for this signature in database
GPG Key ID: BAE12FF60C8A97F2
6 changed files with 184 additions and 86 deletions

45
main.go
View File

@ -195,9 +195,39 @@ func main() {
zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType) zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter) zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
var awsSession *session.Session awsSessions := make(map[string]*session.Session, len(cfg.AWSProfiles))
var awsDefaultSession *session.Session
if cfg.Provider == "aws" || cfg.Provider == "aws-sd" || cfg.Registry == "dynamodb" { if cfg.Provider == "aws" || cfg.Provider == "aws-sd" || cfg.Registry == "dynamodb" {
awsSession, err = aws.NewSession( if len(cfg.AWSProfiles) == 0 || (len(cfg.AWSProfiles) == 1 && cfg.AWSProfiles[0] == "") {
session, err := aws.NewSession(
aws.AWSSessionConfig{
AssumeRole: cfg.AWSAssumeRole,
AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID,
APIRetries: cfg.AWSAPIRetries,
},
)
if err != nil {
log.Fatal(err)
}
awsSessions[aws.DefaultAWSProfile] = session
} else {
for _, profile := range cfg.AWSProfiles {
session, err := aws.NewSession(
aws.AWSSessionConfig{
AssumeRole: cfg.AWSAssumeRole,
AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID,
APIRetries: cfg.AWSAPIRetries,
Profile: profile,
},
)
if err != nil {
log.Fatal(err)
}
awsSessions[profile] = session
}
}
awsDefaultSession, err = aws.NewSession(
aws.AWSSessionConfig{ aws.AWSSessionConfig{
AssumeRole: cfg.AWSAssumeRole, AssumeRole: cfg.AWSAssumeRole,
AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID, AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID,
@ -227,6 +257,11 @@ func main() {
case "alibabacloud": case "alibabacloud":
p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun) p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
case "aws": case "aws":
clients := make(map[string]aws.Route53API, len(awsSessions))
for profile, session := range awsSessions {
clients[profile] = route53.New(session)
}
p, err = aws.NewAWSProvider( p, err = aws.NewAWSProvider(
aws.AWSConfig{ aws.AWSConfig{
DomainFilter: domainFilter, DomainFilter: domainFilter,
@ -243,7 +278,7 @@ func main() {
DryRun: cfg.DryRun, DryRun: cfg.DryRun,
ZoneCacheDuration: cfg.AWSZoneCacheDuration, ZoneCacheDuration: cfg.AWSZoneCacheDuration,
}, },
route53.New(awsSession), clients,
) )
case "aws-sd": case "aws-sd":
// Check that only compatible Registry is used with AWS-SD // Check that only compatible Registry is used with AWS-SD
@ -251,7 +286,7 @@ func main() {
log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry) log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry)
cfg.Registry = "aws-sd" cfg.Registry = "aws-sd"
} }
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, sd.New(awsSession)) p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, sd.New(awsDefaultSession))
case "azure-dns", "azure": case "azure-dns", "azure":
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "azure-private-dns": case "azure-private-dns":
@ -438,7 +473,7 @@ func main() {
if cfg.AWSDynamoDBRegion != "" { if cfg.AWSDynamoDBRegion != "" {
config = config.WithRegion(cfg.AWSDynamoDBRegion) config = config.WithRegion(cfg.AWSDynamoDBRegion)
} }
r, err = registry.NewDynamoDBRegistry(p, cfg.TXTOwnerID, dynamodb.New(awsSession, config), cfg.AWSDynamoDBTable, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, []byte(cfg.TXTEncryptAESKey), cfg.TXTCacheInterval) r, err = registry.NewDynamoDBRegistry(p, cfg.TXTOwnerID, dynamodb.New(awsDefaultSession, config), cfg.AWSDynamoDBTable, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, []byte(cfg.TXTEncryptAESKey), cfg.TXTCacheInterval)
case "noop": case "noop":
r, err = registry.NewNoopRegistry(p) r, err = registry.NewNoopRegistry(p)
case "txt": case "txt":

View File

@ -84,6 +84,7 @@ type Config struct {
AWSZoneType string AWSZoneType string
AWSZoneTagFilter []string AWSZoneTagFilter []string
AWSAssumeRole string AWSAssumeRole string
AWSProfiles []string
AWSAssumeRoleExternalID string AWSAssumeRoleExternalID string
AWSBatchChangeSize int AWSBatchChangeSize int
AWSBatchChangeSizeBytes int AWSBatchChangeSizeBytes int
@ -490,6 +491,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AlibabaCloudZoneType).EnumVar(&cfg.AlibabaCloudZoneType, "", "public", "private") app.Flag("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AlibabaCloudZoneType).EnumVar(&cfg.AlibabaCloudZoneType, "", "public", "private")
app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private") app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private")
app.Flag("aws-zone-tags", "When using the AWS provider, filter for zones with these tags").Default("").StringsVar(&cfg.AWSZoneTagFilter) app.Flag("aws-zone-tags", "When using the AWS provider, filter for zones with these tags").Default("").StringsVar(&cfg.AWSZoneTagFilter)
app.Flag("aws-profile", "When using the AWS provider, name of the profile to use").Default("").StringsVar(&cfg.AWSProfiles)
app.Flag("aws-assume-role", "When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole) app.Flag("aws-assume-role", "When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole)
app.Flag("aws-assume-role-external-id", "When using the AWS API and assuming a role then specify this external ID` (optional)").Default(defaultConfig.AWSAssumeRoleExternalID).StringVar(&cfg.AWSAssumeRoleExternalID) app.Flag("aws-assume-role-external-id", "When using the AWS API and assuming a role then specify this external ID` (optional)").Default(defaultConfig.AWSAssumeRoleExternalID).StringVar(&cfg.AWSAssumeRoleExternalID)
app.Flag("aws-batch-change-size", "When using the AWS provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSize)).IntVar(&cfg.AWSBatchChangeSize) app.Flag("aws-batch-change-size", "When using the AWS provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSize)).IntVar(&cfg.AWSBatchChangeSize)

View File

@ -65,6 +65,7 @@ var (
AWSEvaluateTargetHealth: true, AWSEvaluateTargetHealth: true,
AWSAPIRetries: 3, AWSAPIRetries: 3,
AWSPreferCNAME: false, AWSPreferCNAME: false,
AWSProfiles: []string{""},
AWSZoneCacheDuration: 0 * time.Second, AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false, AWSSDServiceCleanup: false,
AWSDynamoDBTable: "external-dns", AWSDynamoDBTable: "external-dns",
@ -179,6 +180,7 @@ var (
AWSEvaluateTargetHealth: false, AWSEvaluateTargetHealth: false,
AWSAPIRetries: 13, AWSAPIRetries: 13,
AWSPreferCNAME: true, AWSPreferCNAME: true,
AWSProfiles: []string{"profile1", "profile2"},
AWSZoneCacheDuration: 10 * time.Second, AWSZoneCacheDuration: 10 * time.Second,
AWSSDServiceCleanup: true, AWSSDServiceCleanup: true,
AWSDynamoDBTable: "custom-table", AWSDynamoDBTable: "custom-table",
@ -366,6 +368,8 @@ func TestParseFlags(t *testing.T) {
"--aws-batch-change-interval=2s", "--aws-batch-change-interval=2s",
"--aws-api-retries=13", "--aws-api-retries=13",
"--aws-prefer-cname", "--aws-prefer-cname",
"--aws-profile=profile1",
"--aws-profile=profile2",
"--aws-zones-cache-duration=10s", "--aws-zones-cache-duration=10s",
"--aws-sd-service-cleanup", "--aws-sd-service-cleanup",
"--no-aws-evaluate-target-health", "--no-aws-evaluate-target-health",
@ -492,6 +496,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0", "EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0",
"EXTERNAL_DNS_AWS_API_RETRIES": "13", "EXTERNAL_DNS_AWS_API_RETRIES": "13",
"EXTERNAL_DNS_AWS_PREFER_CNAME": "true", "EXTERNAL_DNS_AWS_PREFER_CNAME": "true",
"EXTERNAL_DNS_AWS_PROFILE": "profile1\nprofile2",
"EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s", "EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s",
"EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true", "EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true",
"EXTERNAL_DNS_DYNAMODB_TABLE": "custom-table", "EXTERNAL_DNS_DYNAMODB_TABLE": "custom-table",

View File

@ -18,6 +18,7 @@ package aws
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"sort" "sort"
"strconv" "strconv"
@ -25,6 +26,7 @@ import (
"time" "time"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/route53" "github.com/aws/aws-sdk-go/service/route53"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -35,7 +37,8 @@ import (
) )
const ( const (
recordTTL = 300 DefaultAWSProfile = "default"
recordTTL = 300
// From the experiments, it seems that the default MaxItems applied is 100, // From the experiments, it seems that the default MaxItems applied is 100,
// and that, on the server side, there is a hard limit of 300 elements per page. // and that, on the server side, there is a hard limit of 300 elements per page.
// After a discussion with AWS representants, clients should accept // After a discussion with AWS representants, clients should accept
@ -211,6 +214,11 @@ type Route53Change struct {
type Route53Changes []*Route53Change type Route53Changes []*Route53Change
type profiledZone struct {
profile string
zone *route53.HostedZone
}
func (cs Route53Changes) Route53Changes() []*route53.Change { func (cs Route53Changes) Route53Changes() []*route53.Change {
ret := []*route53.Change{} ret := []*route53.Change{}
for _, c := range cs { for _, c := range cs {
@ -222,13 +230,13 @@ func (cs Route53Changes) Route53Changes() []*route53.Change {
type zonesListCache struct { type zonesListCache struct {
age time.Time age time.Time
duration time.Duration duration time.Duration
zones map[string]*route53.HostedZone zones map[string]*profiledZone
} }
// AWSProvider is an implementation of Provider for AWS Route53. // AWSProvider is an implementation of Provider for AWS Route53.
type AWSProvider struct { type AWSProvider struct {
provider.BaseProvider provider.BaseProvider
client Route53API clients map[string]Route53API
dryRun bool dryRun bool
batchChangeSize int batchChangeSize int
batchChangeSizeBytes int batchChangeSizeBytes int
@ -269,9 +277,9 @@ type AWSConfig struct {
} }
// NewAWSProvider initializes a new AWS Route53 based Provider. // NewAWSProvider initializes a new AWS Route53 based Provider.
func NewAWSProvider(awsConfig AWSConfig, client Route53API) (*AWSProvider, error) { func NewAWSProvider(awsConfig AWSConfig, clients map[string]Route53API) (*AWSProvider, error) {
provider := &AWSProvider{ provider := &AWSProvider{
client: client, clients: clients,
domainFilter: awsConfig.DomainFilter, domainFilter: awsConfig.DomainFilter,
zoneIDFilter: awsConfig.ZoneIDFilter, zoneIDFilter: awsConfig.ZoneIDFilter,
zoneTypeFilter: awsConfig.ZoneTypeFilter, zoneTypeFilter: awsConfig.ZoneTypeFilter,
@ -293,14 +301,27 @@ func NewAWSProvider(awsConfig AWSConfig, client Route53API) (*AWSProvider, error
// Zones returns the list of hosted zones. // Zones returns the list of hosted zones.
func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone, error) { func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone, error) {
zones, err := p.zones(ctx)
if err != nil {
return nil, err
}
result := make(map[string]*route53.HostedZone, len(zones))
for id, zone := range zones {
result[id] = zone.zone
}
return result, nil
}
func (p *AWSProvider) zones(ctx context.Context) (map[string]*profiledZone, error) {
if p.zonesCache.zones != nil && time.Since(p.zonesCache.age) < p.zonesCache.duration { if p.zonesCache.zones != nil && time.Since(p.zonesCache.age) < p.zonesCache.duration {
log.Debug("Using cached zones list") log.Debug("Using cached zones list")
return p.zonesCache.zones, nil return p.zonesCache.zones, nil
} }
log.Debug("Refreshing zones list cache") log.Debug("Refreshing zones list cache")
zones := make(map[string]*route53.HostedZone) zones := make(map[string]*profiledZone)
var profile string
var tagErr error var tagErr error
f := func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool) { f := func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool) {
for _, zone := range resp.HostedZones { for _, zone := range resp.HostedZones {
@ -323,7 +344,7 @@ func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone
// Only fetch tags if a tag filter was specified // Only fetch tags if a tag filter was specified
if !p.zoneTagFilter.IsEmpty() { if !p.zoneTagFilter.IsEmpty() {
tags, err := p.tagsForZone(ctx, *zone.Id) tags, err := p.tagsForZone(ctx, *zone.Id, profile)
if err != nil { if err != nil {
tagErr = err tagErr = err
return false return false
@ -333,22 +354,40 @@ func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone
} }
} }
zones[aws.StringValue(zone.Id)] = zone zones[aws.StringValue(zone.Id)] = &profiledZone{
profile: profile,
zone: zone,
}
} }
return true return true
} }
err := p.client.ListHostedZonesPagesWithContext(ctx, &route53.ListHostedZonesInput{}, f) for p, client := range p.clients {
if err != nil { profile = p
return nil, provider.NewSoftError(fmt.Errorf("failed to list hosted zones: %w", err)) err := client.ListHostedZonesPagesWithContext(ctx, &route53.ListHostedZonesInput{}, f)
} if err != nil {
if tagErr != nil { var awsErr awserr.Error
return nil, provider.NewSoftError(fmt.Errorf("failed to list zones tags: %w", tagErr)) if errors.As(err, &awsErr) {
if awsErr.Code() == "AccessDenied" {
log.Warnf("Skipping profile %q due to missing permission: %v", profile, awsErr.Message())
continue
}
if awsErr.Code() == "InvalidClientTokenId" || awsErr.Code() == "ExpiredToken" || awsErr.Code() == "SignatureDoesNotMatch" {
log.Warnf("Skipping profile %q due to credential issues: %v", profile, awsErr.Message())
continue
}
}
return nil, provider.NewSoftError(fmt.Errorf("failed to list hosted zones: %w", err))
}
if tagErr != nil {
return nil, provider.NewSoftError(fmt.Errorf("failed to list zones tags: %w", tagErr))
}
} }
for _, zone := range zones { for _, zone := range zones {
log.Debugf("Considering zone: %s (domain: %s)", aws.StringValue(zone.Id), aws.StringValue(zone.Name)) log.Debugf("Considering zone: %s (domain: %s)", aws.StringValue(zone.zone.Id), aws.StringValue(zone.zone.Name))
} }
if p.zonesCache.duration > time.Duration(0) { if p.zonesCache.duration > time.Duration(0) {
@ -367,7 +406,7 @@ func wildcardUnescape(s string) string {
// Records returns the list of records in a given hosted zone. // Records returns the list of records in a given hosted zone.
func (p *AWSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { func (p *AWSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
zones, err := p.Zones(ctx) zones, err := p.zones(ctx)
if err != nil { if err != nil {
return nil, provider.NewSoftError(fmt.Errorf("records retrieval failed: %w", err)) return nil, provider.NewSoftError(fmt.Errorf("records retrieval failed: %w", err))
} }
@ -375,7 +414,7 @@ func (p *AWSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoi
return p.records(ctx, zones) return p.records(ctx, zones)
} }
func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.HostedZone) ([]*endpoint.Endpoint, error) { func (p *AWSProvider) records(ctx context.Context, zones map[string]*profiledZone) ([]*endpoint.Endpoint, error) {
endpoints := make([]*endpoint.Endpoint, 0) endpoints := make([]*endpoint.Endpoint, 0)
f := func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool) { f := func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool) {
for _, r := range resp.ResourceRecordSets { for _, r := range resp.ResourceRecordSets {
@ -456,12 +495,12 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos
for _, z := range zones { for _, z := range zones {
params := &route53.ListResourceRecordSetsInput{ params := &route53.ListResourceRecordSetsInput{
HostedZoneId: z.Id, HostedZoneId: z.zone.Id,
MaxItems: aws.String(route53PageSize), MaxItems: aws.String(route53PageSize),
} }
if err := p.client.ListResourceRecordSetsPagesWithContext(ctx, params, f); err != nil { if err := p.client.ListResourceRecordSetsPagesWithContext(ctx, params, f); err != nil {
return nil, provider.NewSoftError(fmt.Errorf("failed to list resource records sets for zone %s: %w", *z.Id, err)) return nil, errors.Wrapf(err, "failed to list resource records sets for zone %s", *z.Id)
} }
} }
@ -544,7 +583,7 @@ func (p *AWSProvider) GetDomainFilter() endpoint.DomainFilter {
// ApplyChanges applies a given set of changes in a given zone. // ApplyChanges applies a given set of changes in a given zone.
func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
zones, err := p.Zones(ctx) zones, err := p.zones(ctx)
if err != nil { if err != nil {
return provider.NewSoftError(fmt.Errorf("failed to list zones, not applying changes: %w", err)) return provider.NewSoftError(fmt.Errorf("failed to list zones, not applying changes: %w", err))
} }
@ -560,7 +599,7 @@ func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) e
} }
// submitChanges takes a zone and a collection of Changes and sends them as a single transaction. // submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes, zones map[string]*route53.HostedZone) error { func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes, zones map[string]*profiledZone) error {
// return early if there is nothing to change // return early if there is nothing to change
if len(changes) == 0 { if len(changes) == 0 {
log.Info("All records are already up to date") log.Info("All records are already up to date")
@ -602,8 +641,9 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes,
successfulChanges := 0 successfulChanges := 0
if _, err := p.client.ChangeResourceRecordSetsWithContext(ctx, params); err != nil { client := p.clients[zones[z].profile]
log.Errorf("Failure in zone %s [Id: %s] when submitting change batch: %v", aws.StringValue(zones[z].Name), z, err) if _, err := client.ChangeResourceRecordSetsWithContext(ctx, params); err != nil {
log.Errorf("Failure in zone %s [Id: %s] when submitting change batch: %v", aws.StringValue(zones[z].zone.Name), z, err)
changesByOwnership := groupChangesByNameAndOwnershipRelation(b) changesByOwnership := groupChangesByNameAndOwnershipRelation(b)
@ -617,7 +657,7 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes,
params.ChangeBatch = &route53.ChangeBatch{ params.ChangeBatch = &route53.ChangeBatch{
Changes: changes.Route53Changes(), Changes: changes.Route53Changes(),
} }
if _, err := p.client.ChangeResourceRecordSetsWithContext(ctx, params); err != nil { if _, err := client.ChangeResourceRecordSetsWithContext(ctx, params); err != nil {
failedUpdate = true failedUpdate = true
log.Errorf("Failed submitting change (error: %v), it will be retried in a separate change batch in the next iteration", err) log.Errorf("Failed submitting change (error: %v), it will be retried in a separate change batch in the next iteration", err)
p.failedChangesQueue[z] = append(p.failedChangesQueue[z], changes...) p.failedChangesQueue[z] = append(p.failedChangesQueue[z], changes...)
@ -634,7 +674,7 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes,
if successfulChanges > 0 { if successfulChanges > 0 {
// z is the R53 Hosted Zone ID already as aws.StringValue // z is the R53 Hosted Zone ID already as aws.StringValue
log.Infof("%d record(s) in zone %s [Id: %s] were successfully updated", successfulChanges, aws.StringValue(zones[z].Name), z) log.Infof("%d record(s) in zone %s [Id: %s] were successfully updated", successfulChanges, aws.StringValue(zones[z].zone.Name), z)
} }
if i != len(batchCs)-1 { if i != len(batchCs)-1 {
@ -869,8 +909,10 @@ func groupChangesByNameAndOwnershipRelation(cs Route53Changes) map[string]Route5
return changesByOwnership return changesByOwnership
} }
func (p *AWSProvider) tagsForZone(ctx context.Context, zoneID string) (map[string]string, error) { func (p *AWSProvider) tagsForZone(ctx context.Context, zoneID string, profile string) (map[string]string, error) {
response, err := p.client.ListTagsForResourceWithContext(ctx, &route53.ListTagsForResourceInput{ client := p.clients[profile]
response, err := client.ListTagsForResourceWithContext(ctx, &route53.ListTagsForResourceInput{
ResourceType: aws.String("hostedzone"), ResourceType: aws.String("hostedzone"),
ResourceId: aws.String(zoneID), ResourceId: aws.String(zoneID),
}) })
@ -977,11 +1019,11 @@ func sortChangesByActionNameType(cs Route53Changes) Route53Changes {
} }
// changesByZone separates a multi-zone change into a single change per zone. // changesByZone separates a multi-zone change into a single change per zone.
func changesByZone(zones map[string]*route53.HostedZone, changeSet Route53Changes) map[string]Route53Changes { func changesByZone(zones map[string]*profiledZone, changeSet Route53Changes) map[string]Route53Changes {
changes := make(map[string]Route53Changes) changes := make(map[string]Route53Changes)
for _, z := range zones { for _, z := range zones {
changes[aws.StringValue(z.Id)] = Route53Changes{} changes[aws.StringValue(z.zone.Id)] = Route53Changes{}
} }
for _, c := range changeSet { for _, c := range changeSet {
@ -998,7 +1040,7 @@ func changesByZone(zones map[string]*route53.HostedZone, changeSet Route53Change
// if it's not, this will fail // if it's not, this will fail
rrset := *c.ResourceRecordSet rrset := *c.ResourceRecordSet
aliasTarget := *rrset.AliasTarget aliasTarget := *rrset.AliasTarget
aliasTarget.HostedZoneId = aws.String(cleanZoneID(aws.StringValue(z.Id))) aliasTarget.HostedZoneId = aws.String(cleanZoneID(aws.StringValue(z.zone.Id)))
rrset.AliasTarget = &aliasTarget rrset.AliasTarget = &aliasTarget
c = &Route53Change{ c = &Route53Change{
Change: route53.Change{ Change: route53.Change{
@ -1007,8 +1049,8 @@ func changesByZone(zones map[string]*route53.HostedZone, changeSet Route53Change
}, },
} }
} }
changes[aws.StringValue(z.Id)] = append(changes[aws.StringValue(z.Id)], c) changes[aws.StringValue(z.zone.Id)] = append(changes[aws.StringValue(z.zone.Id)], c)
log.Debugf("Adding %s to zone %s [Id: %s]", hostname, aws.StringValue(z.Name), aws.StringValue(z.Id)) log.Debugf("Adding %s to zone %s [Id: %s]", hostname, aws.StringValue(z.zone.Name), aws.StringValue(z.zone.Id))
} }
} }
@ -1025,15 +1067,15 @@ func changesByZone(zones map[string]*route53.HostedZone, changeSet Route53Change
// suitableZones returns all suitable private zones and the most suitable public zone // suitableZones returns all suitable private zones and the most suitable public zone
// //
// for a given hostname and a set of zones. // for a given hostname and a set of zones.
func suitableZones(hostname string, zones map[string]*route53.HostedZone) []*route53.HostedZone { func suitableZones(hostname string, zones map[string]*profiledZone) []*profiledZone {
var matchingZones []*route53.HostedZone var matchingZones []*profiledZone
var publicZone *route53.HostedZone var publicZone *profiledZone
for _, z := range zones { for _, z := range zones {
if aws.StringValue(z.Name) == hostname || strings.HasSuffix(hostname, "."+aws.StringValue(z.Name)) { if aws.StringValue(z.zone.Name) == hostname || strings.HasSuffix(hostname, "."+aws.StringValue(z.zone.Name)) {
if z.Config == nil || !aws.BoolValue(z.Config.PrivateZone) { if z.zone.Config == nil || !aws.BoolValue(z.zone.Config.PrivateZone) {
// Only select the best matching public zone // Only select the best matching public zone
if publicZone == nil || len(aws.StringValue(z.Name)) > len(aws.StringValue(publicZone.Name)) { if publicZone == nil || len(aws.StringValue(z.zone.Name)) > len(aws.StringValue(publicZone.zone.Name)) {
publicZone = z publicZone = z
} }
} else { } else {

View File

@ -750,14 +750,14 @@ func TestAWSApplyChanges(t *testing.T) {
ctx := tt.setup(provider) ctx := tt.setup(provider)
provider.zonesCache = &zonesListCache{duration: 0 * time.Minute} provider.zonesCache = &zonesListCache{duration: 0 * time.Minute}
counter := NewRoute53APICounter(provider.client) counter := NewRoute53APICounter(provider.clients[DefaultAWSProfile])
provider.client = counter provider.clients[DefaultAWSProfile] = counter
require.NoError(t, provider.ApplyChanges(ctx, changes)) require.NoError(t, provider.ApplyChanges(ctx, changes))
assert.Equal(t, 1, counter.calls["ListHostedZonesPages"], tt.name) assert.Equal(t, 1, counter.calls["ListHostedZonesPages"], tt.name)
assert.Equal(t, tt.listRRSets, counter.calls["ListResourceRecordSetsPages"], tt.name) assert.Equal(t, tt.listRRSets, counter.calls["ListResourceRecordSetsPages"], tt.name)
validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{ validateRecords(t, listAWSRecords(t, provider.clients[DefaultAWSProfile], "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{
{ {
Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
Type: aws.String(route53.RRTypeA), Type: aws.String(route53.RRTypeA),
@ -854,7 +854,7 @@ func TestAWSApplyChanges(t *testing.T) {
ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}}, ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}},
}, },
}) })
validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{ validateRecords(t, listAWSRecords(t, provider.clients[DefaultAWSProfile], "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{
{ {
Name: aws.String("create-test.zone-2.ext-dns-test-2.teapot.zalan.do."), Name: aws.String("create-test.zone-2.ext-dns-test-2.teapot.zalan.do."),
Type: aws.String(route53.RRTypeA), Type: aws.String(route53.RRTypeA),
@ -1023,8 +1023,8 @@ func TestAWSApplyChangesDryRun(t *testing.T) {
validateRecords(t, validateRecords(t,
append( append(
listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), listAWSRecords(t, provider.clients[DefaultAWSProfile], "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."),
listAWSRecords(t, provider.client, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.")...), listAWSRecords(t, provider.clients[DefaultAWSProfile], "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.")...),
originalRecords) originalRecords)
} }
@ -1064,23 +1064,35 @@ func TestAWSChangesByZones(t *testing.T) {
}, },
} }
zones := map[string]*route53.HostedZone{ zones := map[string]*profiledZone{
"foo-example-org": { "foo-example-org": &profiledZone{
Id: aws.String("foo-example-org"), profile: DefaultAWSProfile,
Name: aws.String("foo.example.org."), zone: &route53.HostedZone{
Id: aws.String("foo-example-org"),
Name: aws.String("foo.example.org."),
},
}, },
"bar-example-org": { "bar-example-org": &profiledZone{
Id: aws.String("bar-example-org"), profile: DefaultAWSProfile,
Name: aws.String("bar.example.org."), zone: &route53.HostedZone{
Id: aws.String("bar-example-org"),
Name: aws.String("bar.example.org."),
},
}, },
"bar-example-org-private": { "bar-example-org-private": &profiledZone{
Id: aws.String("bar-example-org-private"), profile: DefaultAWSProfile,
Name: aws.String("bar.example.org."), zone: &route53.HostedZone{
Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}, Id: aws.String("bar-example-org-private"),
Name: aws.String("bar.example.org."),
Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)},
},
}, },
"baz-example-org": { "baz-example-org": &profiledZone{
Id: aws.String("baz-example-org"), profile: DefaultAWSProfile,
Name: aws.String("baz.example.org."), zone: &route53.HostedZone{
Id: aws.String("baz-example-org"),
Name: aws.String("baz.example.org."),
},
}, },
} }
@ -1161,7 +1173,7 @@ func TestAWSsubmitChanges(t *testing.T) {
} }
ctx := context.Background() ctx := context.Background()
zones, _ := provider.Zones(ctx) zones, _ := provider.zones(ctx)
records, _ := provider.Records(ctx) records, _ := provider.Records(ctx)
cs := make(Route53Changes, 0, len(endpoints)) cs := make(Route53Changes, 0, len(endpoints))
cs = append(cs, provider.newChanges(route53.ChangeActionCreate, endpoints)...) cs = append(cs, provider.newChanges(route53.ChangeActionCreate, endpoints)...)
@ -1179,7 +1191,7 @@ func TestAWSsubmitChangesError(t *testing.T) {
clientStub.MockMethod("ChangeResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure")) clientStub.MockMethod("ChangeResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure"))
ctx := context.Background() ctx := context.Background()
zones, err := provider.Zones(ctx) zones, err := provider.zones(ctx)
require.NoError(t, err) require.NoError(t, err)
ep := endpoint.NewEndpointWithTTL("fail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.1") ep := endpoint.NewEndpointWithTTL("fail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.1")
@ -1192,7 +1204,7 @@ func TestAWSsubmitChangesRetryOnError(t *testing.T) {
provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil)
ctx := context.Background() ctx := context.Background()
zones, err := provider.Zones(ctx) zones, err := provider.zones(ctx)
require.NoError(t, err) require.NoError(t, err)
ep1 := endpoint.NewEndpointWithTTL("success.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.1") ep1 := endpoint.NewEndpointWithTTL("success.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.1")
@ -1636,7 +1648,7 @@ func TestAWSCreateRecordsWithCNAME(t *testing.T) {
Create: adjusted, Create: adjusted,
})) }))
recordSets := listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") recordSets := listAWSRecords(t, provider.clients[DefaultAWSProfile], "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.")
validateRecords(t, recordSets, []*route53.ResourceRecordSet{ validateRecords(t, recordSets, []*route53.ResourceRecordSet{
{ {
@ -1700,7 +1712,7 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) {
Create: adjusted, Create: adjusted,
})) }))
recordSets := listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") recordSets := listAWSRecords(t, provider.clients[DefaultAWSProfile], "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.")
validateRecords(t, recordSets, []*route53.ResourceRecordSet{ validateRecords(t, recordSets, []*route53.ResourceRecordSet{
{ {
@ -1789,40 +1801,40 @@ func TestAWSCanonicalHostedZone(t *testing.T) {
} }
func TestAWSSuitableZones(t *testing.T) { func TestAWSSuitableZones(t *testing.T) {
zones := map[string]*route53.HostedZone{ zones := map[string]*profiledZone{
// Public domain // Public domain
"example-org": {Id: aws.String("example-org"), Name: aws.String("example.org.")}, "example-org": {profile: DefaultAWSProfile, zone: &route53.HostedZone{Id: aws.String("example-org"), Name: aws.String("example.org.")}},
// Public subdomain // Public subdomain
"bar-example-org": {Id: aws.String("bar-example-org"), Name: aws.String("bar.example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}}, "bar-example-org": {profile: DefaultAWSProfile, zone: &route53.HostedZone{Id: aws.String("bar-example-org"), Name: aws.String("bar.example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}}},
// Public subdomain // Public subdomain
"longfoo-bar-example-org": {Id: aws.String("longfoo-bar-example-org"), Name: aws.String("longfoo.bar.example.org.")}, "longfoo-bar-example-org": {profile: DefaultAWSProfile, zone: &route53.HostedZone{Id: aws.String("longfoo-bar-example-org"), Name: aws.String("longfoo.bar.example.org.")}},
// Private domain // Private domain
"example-org-private": {Id: aws.String("example-org-private"), Name: aws.String("example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}}, "example-org-private": {profile: DefaultAWSProfile, zone: &route53.HostedZone{Id: aws.String("example-org-private"), Name: aws.String("example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}}},
// Private subdomain // Private subdomain
"bar-example-org-private": {Id: aws.String("bar-example-org-private"), Name: aws.String("bar.example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}}, "bar-example-org-private": {profile: DefaultAWSProfile, zone: &route53.HostedZone{Id: aws.String("bar-example-org-private"), Name: aws.String("bar.example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}}},
} }
for _, tc := range []struct { for _, tc := range []struct {
hostname string hostname string
expected []*route53.HostedZone expected []*profiledZone
}{ }{
// bar.example.org is NOT suitable // bar.example.org is NOT suitable
{"foobar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["example-org"]}}, {"foobar.example.org.", []*profiledZone{zones["example-org-private"], zones["example-org"]}},
// all matching private zones are suitable // all matching private zones are suitable
// https://github.com/kubernetes-sigs/external-dns/pull/356 // https://github.com/kubernetes-sigs/external-dns/pull/356
{"bar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}}, {"bar.example.org.", []*profiledZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}},
{"foo.bar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}}, {"foo.bar.example.org.", []*profiledZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}},
{"foo.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["example-org"]}}, {"foo.example.org.", []*profiledZone{zones["example-org-private"], zones["example-org"]}},
{"foo.kubernetes.io.", nil}, {"foo.kubernetes.io.", nil},
} { } {
suitableZones := suitableZones(tc.hostname, zones) suitableZones := suitableZones(tc.hostname, zones)
sort.Slice(suitableZones, func(i, j int) bool { sort.Slice(suitableZones, func(i, j int) bool {
return *suitableZones[i].Id < *suitableZones[j].Id return *suitableZones[i].zone.Id < *suitableZones[j].zone.Id
}) })
sort.Slice(tc.expected, func(i, j int) bool { sort.Slice(tc.expected, func(i, j int) bool {
return *tc.expected[i].Id < *tc.expected[j].Id return *tc.expected[i].zone.Id < *tc.expected[j].zone.Id
}) })
assert.Equal(t, tc.expected, suitableZones) assert.Equal(t, tc.expected, suitableZones)
} }
@ -1835,7 +1847,7 @@ func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone
HostedZoneConfig: zone.Config, HostedZoneConfig: zone.Config,
} }
if _, err := provider.client.CreateHostedZoneWithContext(context.Background(), params); err != nil { if _, err := provider.clients[DefaultAWSProfile].CreateHostedZoneWithContext(context.Background(), params); err != nil {
require.EqualError(t, err, route53.ErrCodeHostedZoneAlreadyExists) require.EqualError(t, err, route53.ErrCodeHostedZoneAlreadyExists)
} }
} }
@ -1863,7 +1875,7 @@ func setAWSRecords(t *testing.T, provider *AWSProvider, records []*route53.Resou
}) })
} }
zones, err := provider.Zones(ctx) zones, err := provider.zones(ctx)
require.NoError(t, err) require.NoError(t, err)
err = provider.submitChanges(ctx, changes, zones) err = provider.submitChanges(ctx, changes, zones)
require.NoError(t, err) require.NoError(t, err)
@ -1893,7 +1905,7 @@ func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilte
client := NewRoute53APIStub(t) client := NewRoute53APIStub(t)
provider := &AWSProvider{ provider := &AWSProvider{
client: client, clients: map[string]Route53API{DefaultAWSProfile: client},
batchChangeSize: defaultBatchChangeSize, batchChangeSize: defaultBatchChangeSize,
batchChangeSizeBytes: defaultBatchChangeSizeBytes, batchChangeSizeBytes: defaultBatchChangeSizeBytes,
batchChangeSizeValues: defaultBatchChangeSizeValues, batchChangeSizeValues: defaultBatchChangeSizeValues,
@ -1933,7 +1945,7 @@ func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilte
Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}, Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)},
}) })
setupZoneTags(provider.client.(*Route53APIStub)) setupZoneTags(provider.clients[DefaultAWSProfile].(*Route53APIStub))
setAWSRecords(t, provider, records) setAWSRecords(t, provider, records)

View File

@ -35,6 +35,7 @@ type AWSSessionConfig struct {
AssumeRole string AssumeRole string
AssumeRoleExternalID string AssumeRoleExternalID string
APIRetries int APIRetries int
Profile string
} }
func NewSession(awsConfig AWSSessionConfig) (*session.Session, error) { func NewSession(awsConfig AWSSessionConfig) (*session.Session, error) {
@ -52,6 +53,7 @@ func NewSession(awsConfig AWSSessionConfig) (*session.Session, error) {
session, err := session.NewSessionWithOptions(session.Options{ session, err := session.NewSessionWithOptions(session.Options{
Config: *config, Config: *config,
SharedConfigState: session.SharedConfigEnable, SharedConfigState: session.SharedConfigEnable,
Profile: awsConfig.Profile,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("instantiating AWS session: %w", err) return nil, fmt.Errorf("instantiating AWS session: %w", err)