From 2815f4856a42bace48512ddc845c8b509573927d Mon Sep 17 00:00:00 2001 From: Jan Roehrich Date: Thu, 5 Oct 2023 13:29:14 +0200 Subject: [PATCH] Allow usage of (multiple) AWS profiles using .credentials file --- main.go | 34 ++++++++-- pkg/apis/externaldns/types.go | 2 + provider/aws/aws.go | 122 +++++++++++++++++++++++----------- provider/aws/aws_test.go | 112 +++++++++++++++++-------------- provider/aws/session.go | 2 + 5 files changed, 176 insertions(+), 96 deletions(-) diff --git a/main.go b/main.go index 3a801dce66..e49bf67787 100644 --- a/main.go +++ b/main.go @@ -192,9 +192,26 @@ func main() { zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType) 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" || cfg.RunAWSProviderAsWebhook { - awsSession, err = aws.NewSession( + 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{ AssumeRole: cfg.AWSAssumeRole, AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID, @@ -224,6 +241,11 @@ func main() { case "alibabacloud": p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun) case "aws": + clients := make(map[string]aws.Route53API, len(awsSessions)) + for profile, session := range awsSessions { + clients[profile] = route53.New(session) + } + p, err = aws.NewAWSProvider( aws.AWSConfig{ DomainFilter: domainFilter, @@ -237,7 +259,7 @@ func main() { DryRun: cfg.DryRun, ZoneCacheDuration: cfg.AWSZoneCacheDuration, }, - route53.New(awsSession), + clients, ) case "aws-sd": // Check that only compatible Registry is used with AWS-SD @@ -245,7 +267,7 @@ func main() { log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry) 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": p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) case "azure-private-dns": @@ -417,7 +439,7 @@ func main() { PreferCNAME: cfg.AWSPreferCNAME, DryRun: cfg.DryRun, ZoneCacheDuration: cfg.AWSZoneCacheDuration, - }, route53.New(awsSession)) + }, map[string]aws.Route53API{aws.DefaultAWSProfile: route53.New(awsDefaultSession)}) if awsErr != nil { log.Fatal(awsErr) } @@ -444,7 +466,7 @@ func main() { if 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": r, err = registry.NewNoopRegistry(p) case "txt": diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 87ae51a911..f16dd18f9a 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -84,6 +84,7 @@ type Config struct { AWSZoneType string AWSZoneTagFilter []string AWSAssumeRole string + AWSProfiles []string AWSAssumeRoleExternalID string AWSBatchChangeSize int AWSBatchChangeInterval time.Duration @@ -471,6 +472,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("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-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-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) diff --git a/provider/aws/aws.go b/provider/aws/aws.go index ec3b3187dd..bb16c80c64 100644 --- a/provider/aws/aws.go +++ b/provider/aws/aws.go @@ -25,6 +25,7 @@ import ( "time" "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/service/route53" "github.com/pkg/errors" @@ -36,7 +37,8 @@ import ( ) const ( - recordTTL = 300 + DefaultAWSProfile = "default" + recordTTL = 300 // 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. // After a discussion with AWS representants, clients should accept @@ -208,6 +210,11 @@ type Route53Change struct { type Route53Changes []*Route53Change +type profiledZone struct { + profile string + zone *route53.HostedZone +} + func (cs Route53Changes) Route53Changes() []*route53.Change { ret := []*route53.Change{} for _, c := range cs { @@ -219,13 +226,13 @@ func (cs Route53Changes) Route53Changes() []*route53.Change { type zonesListCache struct { age time.Time duration time.Duration - zones map[string]*route53.HostedZone + zones map[string]*profiledZone } // AWSProvider is an implementation of Provider for AWS Route53. type AWSProvider struct { provider.BaseProvider - client Route53API + clients map[string]Route53API dryRun bool batchChangeSize int batchChangeInterval time.Duration @@ -259,9 +266,9 @@ type AWSConfig struct { } // 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{ - client: client, + clients: clients, domainFilter: awsConfig.DomainFilter, zoneIDFilter: awsConfig.ZoneIDFilter, zoneTypeFilter: awsConfig.ZoneTypeFilter, @@ -280,14 +287,27 @@ func NewAWSProvider(awsConfig AWSConfig, client Route53API) (*AWSProvider, error // Zones returns the list of hosted zones. 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 { log.Debug("Using cached zones list") return p.zonesCache.zones, nil } log.Debug("Refreshing zones list cache") - zones := make(map[string]*route53.HostedZone) - + zones := make(map[string]*profiledZone) + var profile string var tagErr error f := func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool) { for _, zone := range resp.HostedZones { @@ -305,7 +325,7 @@ func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone // Only fetch tags if a tag filter was specified if !p.zoneTagFilter.IsEmpty() { - tags, err := p.tagsForZone(ctx, *zone.Id) + tags, err := p.tagsForZone(ctx, *zone.Id, profile) if err != nil { tagErr = err return false @@ -315,22 +335,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 } - err := p.client.ListHostedZonesPagesWithContext(ctx, &route53.ListHostedZonesInput{}, f) - if err != nil { - return nil, errors.Wrap(err, "failed to list hosted zones") - } - if tagErr != nil { - return nil, errors.Wrap(tagErr, "failed to list zones tags") + for p, client := range p.clients { + profile = p + err := client.ListHostedZonesPagesWithContext(ctx, &route53.ListHostedZonesInput{}, f) + if err != nil { + var awsErr awserr.Error + 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, fmt.Errorf("failed to list hosted zones with profile %q: %w", profile, err) + } + if tagErr != nil { + return nil, fmt.Errorf("failed to list zones tags with profile %q: %w", profile, err) + } + } 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) { @@ -349,7 +387,7 @@ func wildcardUnescape(s string) string { // Records returns the list of records in a given hosted zone. func (p *AWSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { - zones, err := p.Zones(ctx) + zones, err := p.zones(ctx) if err != nil { return nil, errors.Wrap(err, "records retrieval failed") } @@ -357,7 +395,7 @@ func (p *AWSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoi 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) f := func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool) { for _, r := range resp.ResourceRecordSets { @@ -438,12 +476,13 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos for _, z := range zones { params := &route53.ListResourceRecordSetsInput{ - HostedZoneId: z.Id, + HostedZoneId: z.zone.Id, MaxItems: aws.String(route53PageSize), } - if err := p.client.ListResourceRecordSetsPagesWithContext(ctx, params, f); err != nil { - return nil, errors.Wrapf(err, "failed to list resource records sets for zone %s", *z.Id) + client := p.clients[z.profile] + if err := client.ListResourceRecordSetsPagesWithContext(ctx, params, f); err != nil { + return nil, errors.Wrapf(err, "failed to list resource records sets for zone %s usinf profile %q", *z.zone.Id, z.profile) } } @@ -526,7 +565,7 @@ func (p *AWSProvider) GetDomainFilter() endpoint.DomainFilter { // ApplyChanges applies a given set of changes in a given zone. func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - zones, err := p.Zones(ctx) + zones, err := p.zones(ctx) if err != nil { return errors.Wrap(err, "failed to list zones, not applying changes") } @@ -542,7 +581,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. -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 if len(changes) == 0 { log.Info("All records are already up to date") @@ -583,8 +622,9 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes, successfulChanges := 0 - if _, err := p.client.ChangeResourceRecordSetsWithContext(ctx, params); err != nil { - log.Errorf("Failure in zone %s [Id: %s] when submitting change batch: %v", aws.StringValue(zones[z].Name), z, err) + client := p.clients[zones[z].profile] + 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) @@ -598,7 +638,7 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes, params.ChangeBatch = &route53.ChangeBatch{ Changes: changes.Route53Changes(), } - if _, err := p.client.ChangeResourceRecordSetsWithContext(ctx, params); err != nil { + if _, err := client.ChangeResourceRecordSetsWithContext(ctx, params); err != nil { failedUpdate = true 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...) @@ -615,7 +655,7 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes, if successfulChanges > 0 { // 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 { @@ -839,8 +879,10 @@ func groupChangesByNameAndOwnershipRelation(cs Route53Changes) map[string]Route5 return changesByOwnership } -func (p *AWSProvider) tagsForZone(ctx context.Context, zoneID string) (map[string]string, error) { - response, err := p.client.ListTagsForResourceWithContext(ctx, &route53.ListTagsForResourceInput{ +func (p *AWSProvider) tagsForZone(ctx context.Context, zoneID string, profile string) (map[string]string, error) { + client := p.clients[profile] + + response, err := client.ListTagsForResourceWithContext(ctx, &route53.ListTagsForResourceInput{ ResourceType: aws.String("hostedzone"), ResourceId: aws.String(zoneID), }) @@ -916,11 +958,11 @@ func sortChangesByActionNameType(cs Route53Changes) Route53Changes { } // 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) for _, z := range zones { - changes[aws.StringValue(z.Id)] = Route53Changes{} + changes[aws.StringValue(z.zone.Id)] = Route53Changes{} } for _, c := range changeSet { @@ -937,7 +979,7 @@ func changesByZone(zones map[string]*route53.HostedZone, changeSet Route53Change // if it's not, this will fail rrset := *c.ResourceRecordSet 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 c = &Route53Change{ Change: route53.Change{ @@ -946,8 +988,8 @@ func changesByZone(zones map[string]*route53.HostedZone, changeSet Route53Change }, } } - changes[aws.StringValue(z.Id)] = append(changes[aws.StringValue(z.Id)], c) - log.Debugf("Adding %s to zone %s [Id: %s]", hostname, aws.StringValue(z.Name), aws.StringValue(z.Id)) + 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.zone.Name), aws.StringValue(z.zone.Id)) } } @@ -964,15 +1006,15 @@ func changesByZone(zones map[string]*route53.HostedZone, changeSet Route53Change // suitableZones returns all suitable private zones and the most suitable public zone // // for a given hostname and a set of zones. -func suitableZones(hostname string, zones map[string]*route53.HostedZone) []*route53.HostedZone { - var matchingZones []*route53.HostedZone - var publicZone *route53.HostedZone +func suitableZones(hostname string, zones map[string]*profiledZone) []*profiledZone { + var matchingZones []*profiledZone + var publicZone *profiledZone for _, z := range zones { - if aws.StringValue(z.Name) == hostname || strings.HasSuffix(hostname, "."+aws.StringValue(z.Name)) { - if z.Config == nil || !aws.BoolValue(z.Config.PrivateZone) { + if aws.StringValue(z.zone.Name) == hostname || strings.HasSuffix(hostname, "."+aws.StringValue(z.zone.Name)) { + if z.zone.Config == nil || !aws.BoolValue(z.zone.Config.PrivateZone) { // 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 } } else { diff --git a/provider/aws/aws_test.go b/provider/aws/aws_test.go index 13355b6787..8d46aef479 100644 --- a/provider/aws/aws_test.go +++ b/provider/aws/aws_test.go @@ -561,7 +561,7 @@ func TestAWSCreateRecords(t *testing.T) { Create: records, })) - 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."), Type: aws.String(route53.RRTypeA), @@ -602,7 +602,7 @@ func TestAWSCreateRecords(t *testing.T) { ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.example.com")}, {Value: aws.String("20 mailhost2.example.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."), Type: aws.String(route53.RRTypeA), @@ -683,7 +683,7 @@ func TestAWSUpdateRecords(t *testing.T) { UpdateNew: updatedRecords, })) - 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("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: aws.String(route53.RRTypeA), @@ -715,7 +715,7 @@ func TestAWSUpdateRecords(t *testing.T) { ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}, {Value: aws.String("20 mailhost2.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("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: aws.String(route53.RRTypeA), @@ -799,8 +799,8 @@ func TestAWSDeleteRecords(t *testing.T) { }, })) - validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{}) - 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-1.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{}) } func TestAWSApplyChanges(t *testing.T) { @@ -1006,14 +1006,14 @@ func TestAWSApplyChanges(t *testing.T) { ctx := tt.setup(provider) provider.zonesCache = &zonesListCache{duration: 0 * time.Minute} - counter := NewRoute53APICounter(provider.client) - provider.client = counter + counter := NewRoute53APICounter(provider.clients[DefaultAWSProfile]) + provider.clients[DefaultAWSProfile] = counter require.NoError(t, provider.ApplyChanges(ctx, changes)) assert.Equal(t, 1, counter.calls["ListHostedZonesPages"], 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."), Type: aws.String(route53.RRTypeA), @@ -1110,7 +1110,7 @@ func TestAWSApplyChanges(t *testing.T) { 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."), Type: aws.String(route53.RRTypeA), @@ -1279,8 +1279,8 @@ func TestAWSApplyChangesDryRun(t *testing.T) { validateRecords(t, append( - listAWSRecords(t, provider.client, "/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-1.ext-dns-test-2.teapot.zalan.do."), + listAWSRecords(t, provider.clients[DefaultAWSProfile], "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.")...), originalRecords) } @@ -1320,23 +1320,35 @@ func TestAWSChangesByZones(t *testing.T) { }, } - zones := map[string]*route53.HostedZone{ - "foo-example-org": { - Id: aws.String("foo-example-org"), - Name: aws.String("foo.example.org."), + zones := map[string]*profiledZone{ + "foo-example-org": &profiledZone{ + profile: DefaultAWSProfile, + zone: &route53.HostedZone{ + Id: aws.String("foo-example-org"), + Name: aws.String("foo.example.org."), + }, }, - "bar-example-org": { - Id: aws.String("bar-example-org"), - Name: aws.String("bar.example.org."), + "bar-example-org": &profiledZone{ + profile: DefaultAWSProfile, + zone: &route53.HostedZone{ + Id: aws.String("bar-example-org"), + Name: aws.String("bar.example.org."), + }, }, - "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": &profiledZone{ + 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)}, + }, }, - "baz-example-org": { - Id: aws.String("baz-example-org"), - Name: aws.String("baz.example.org."), + "baz-example-org": &profiledZone{ + profile: DefaultAWSProfile, + zone: &route53.HostedZone{ + Id: aws.String("baz-example-org"), + Name: aws.String("baz.example.org."), + }, }, } @@ -1417,7 +1429,7 @@ func TestAWSsubmitChanges(t *testing.T) { } ctx := context.Background() - zones, _ := provider.Zones(ctx) + zones, _ := provider.zones(ctx) records, _ := provider.Records(ctx) cs := make(Route53Changes, 0, len(endpoints)) cs = append(cs, provider.newChanges(route53.ChangeActionCreate, endpoints)...) @@ -1435,7 +1447,7 @@ func TestAWSsubmitChangesError(t *testing.T) { clientStub.MockMethod("ChangeResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure")) ctx := context.Background() - zones, err := provider.Zones(ctx) + zones, err := provider.zones(ctx) 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") @@ -1448,7 +1460,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) ctx := context.Background() - zones, err := provider.Zones(ctx) + zones, err := provider.zones(ctx) 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") @@ -1654,7 +1666,7 @@ func TestAWSCreateRecordsWithCNAME(t *testing.T) { Create: records, })) - 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{ { @@ -1717,7 +1729,7 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) { Create: records, })) - 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{ { @@ -1806,40 +1818,40 @@ func TestAWSCanonicalHostedZone(t *testing.T) { } func TestAWSSuitableZones(t *testing.T) { - zones := map[string]*route53.HostedZone{ + zones := map[string]*profiledZone{ // 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 - "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 - "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 - "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 - "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 { hostname string - expected []*route53.HostedZone + expected []*profiledZone }{ // 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 // 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.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["example-org"]}}, + {"foo.bar.example.org.", []*profiledZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}}, + {"foo.example.org.", []*profiledZone{zones["example-org-private"], zones["example-org"]}}, {"foo.kubernetes.io.", nil}, } { suitableZones := suitableZones(tc.hostname, zones) 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 { - 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) } @@ -1852,7 +1864,7 @@ func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone 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) } } @@ -1880,7 +1892,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) err = provider.submitChanges(ctx, changes, zones) require.NoError(t, err) @@ -1904,7 +1916,7 @@ func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.Res // Route53 stores wildcards escaped: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html?shortFooter=true#domain-name-format-asterisk func escapeAWSRecords(t *testing.T, provider *AWSProvider, zone string) { - recordSets := listAWSRecords(t, provider.client, zone) + recordSets := listAWSRecords(t, provider.clients[DefaultAWSProfile], zone) changes := make([]*route53.Change, 0, len(recordSets)) for _, recordSet := range recordSets { @@ -1915,7 +1927,7 @@ func escapeAWSRecords(t *testing.T, provider *AWSProvider, zone string) { } if len(changes) != 0 { - _, err := provider.client.ChangeResourceRecordSetsWithContext(context.Background(), &route53.ChangeResourceRecordSetsInput{ + _, err := provider.clients[DefaultAWSProfile].ChangeResourceRecordSetsWithContext(context.Background(), &route53.ChangeResourceRecordSetsInput{ HostedZoneId: aws.String(zone), ChangeBatch: &route53.ChangeBatch{ Changes: changes, @@ -1933,7 +1945,7 @@ func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilte client := NewRoute53APIStub(t) provider := &AWSProvider{ - client: client, + clients: map[string]Route53API{DefaultAWSProfile: client}, batchChangeSize: defaultBatchChangeSize, batchChangeInterval: defaultBatchChangeInterval, evaluateTargetHealth: evaluateTargetHealth, @@ -1971,7 +1983,7 @@ func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilte Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}, }) - setupZoneTags(provider.client.(*Route53APIStub)) + setupZoneTags(provider.clients[DefaultAWSProfile].(*Route53APIStub)) setAWSRecords(t, provider, records) diff --git a/provider/aws/session.go b/provider/aws/session.go index 822ab4aff2..f91e051ba2 100644 --- a/provider/aws/session.go +++ b/provider/aws/session.go @@ -35,6 +35,7 @@ type AWSSessionConfig struct { AssumeRole string AssumeRoleExternalID string APIRetries int + Profile string } func NewSession(awsConfig AWSSessionConfig) (*session.Session, error) { @@ -52,6 +53,7 @@ func NewSession(awsConfig AWSSessionConfig) (*session.Session, error) { session, err := session.NewSessionWithOptions(session.Options{ Config: *config, SharedConfigState: session.SharedConfigEnable, + Profile: awsConfig.Profile, }) if err != nil { return nil, fmt.Errorf("instantiating AWS session: %w", err)