Skip to content

Commit ddc77cd

Browse files
committed
resolve pr comments for multi account tgb
1 parent 369bdac commit ddc77cd

20 files changed

+173
-147
lines changed

apis/elbv2/v1alpha1/targetgroupbinding_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ type TargetGroupBindingSpec struct {
128128
// networking provides the networking setup for ELBV2 LoadBalancer to access targets in TargetGroup.
129129
// +optional
130130
Networking *TargetGroupBindingNetworking `json:"networking,omitempty"`
131+
132+
// IAM Role ARN to assume when calling AWS APIs. Useful if the target group is in a different AWS account
133+
// +optional
134+
IamRoleArnToAssume string `json:"iamRoleArnToAssume,omitempty"`
135+
136+
// IAM Role ARN to assume when calling AWS APIs. Needed to assume a role in another account and prevent the confused deputy problem. https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html
137+
// +optional
138+
AssumeRoleExternalId string `json:"assumeRoleExternalId,omitempty"`
131139
}
132140

133141
// TargetGroupBindingStatus defines the observed state of TargetGroupBinding

apis/elbv2/v1beta1/targetgroupbinding_types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,11 @@ type TargetGroupBindingSpec struct {
160160

161161
// IAM Role ARN to assume when calling AWS APIs. Useful if the target group is in a different AWS account
162162
// +optional
163-
IamRoleArnToAssume string `json:"-"` // `json:"iamRoleArnToAssume,omitempty"`
163+
IamRoleArnToAssume string `json:"iamRoleArnToAssume,omitempty"`
164164

165165
// IAM Role ARN to assume when calling AWS APIs. Needed to assume a role in another account and prevent the confused deputy problem. https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html
166166
// +optional
167-
AssumeRoleExternalId string `json:"-"` // `json:"assumeRoleExternalId,omitempty"`
167+
AssumeRoleExternalId string `json:"assumeRoleExternalId,omitempty"`
168168
}
169169

170170
// TargetGroupBindingStatus defines the observed state of TargetGroupBinding

config/crd/bases/elbv2.k8s.aws_targetgroupbindings.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ spec:
6565
spec:
6666
description: TargetGroupBindingSpec defines the desired state of TargetGroupBinding
6767
properties:
68+
assumeRoleExternalId:
69+
description: IAM Role ARN to assume when calling AWS APIs. Needed
70+
to assume a role in another account and prevent the confused deputy
71+
problem. https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html
72+
type: string
73+
iamRoleArnToAssume:
74+
description: IAM Role ARN to assume when calling AWS APIs. Useful
75+
if the target group is in a different AWS account
76+
type: string
6877
multiClusterTargetGroup:
6978
description: MultiClusterTargetGroup Denotes if the TargetGroup is
7079
shared among multiple clusters
@@ -242,6 +251,15 @@ spec:
242251
spec:
243252
description: TargetGroupBindingSpec defines the desired state of TargetGroupBinding
244253
properties:
254+
assumeRoleExternalId:
255+
description: IAM Role ARN to assume when calling AWS APIs. Needed
256+
to assume a role in another account and prevent the confused deputy
257+
problem. https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html
258+
type: string
259+
iamRoleArnToAssume:
260+
description: IAM Role ARN to assume when calling AWS APIs. Useful
261+
if the target group is in a different AWS account
262+
type: string
245263
ipAddressType:
246264
description: ipAddressType specifies whether the target group is of
247265
type IPv4 or IPv6. If unspecified, it will be automatically inferred.

docs/guide/targetgroupbinding/spec.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,6 @@ Kubernetes meta/v1.ObjectMeta
5555
<table>
5656
<tr><td><code>annotations</code></td><td>
5757

58-
<table>
59-
<tr><td><code>alb.ingress.kubernetes.io/IamRoleArnToAssume</code><br><i>string</i></td>
60-
<td><i>(Optional)</i> In case the target group is in a differet AWS account, you put here the role that needs to be assumed in order to manipulate the target group.
61-
</td></tr>
62-
<tr><td><code>alb.ingress.kubernetes.io/AssumeRoleExternalId</code><br><i>string</i></td>
63-
<td><i>(Optional)</i> The external ID for the assume role operation. Optional, but recommended. It helps you to prevent the <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html" target="_blank">confused deputy problem</a>.
64-
</td></tr>
65-
</table>
66-
6758
<tr><td colspan=2>
6859
Refer to the Kubernetes API documentation for the other fields of the
6960
<code>metadata</code> field.

docs/guide/targetgroupbinding/targetgroupbinding.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,10 @@ spec:
112112
### AssumeRole
113113

114114
Sometimes the AWS LoadBalancer controller needs to manipulate target groups from different AWS accounts.
115-
The way to do that is assuming a role from such account. There are annotations that can help you with that:
115+
The way to do that is assuming a role from such account. The following spec fields help you with that.
116116

117-
* `alb.ingress.kubernetes.io/IamRoleArnToAssume`: the ARN that you need to assume
118-
* `alb.ingress.kubernetes.io/AssumeRoleExternalId`: the external ID for the assume role operation. Optional, but recommended. It helps you to prevent the confused deputy problem ( https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html )
117+
* `iamRoleArnToAssume`: the ARN that you need to assume
118+
* `assumeRoleExternalId`: the external ID for the assume role operation. Optional, but recommended. It helps you to prevent the confused deputy problem ( https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html )
119119

120120

121121
## Sample YAML
@@ -125,10 +125,9 @@ apiVersion: elbv2.k8s.aws/v1beta1
125125
kind: TargetGroupBinding
126126
metadata:
127127
name: my-tgb
128-
annotations:
129-
alb.ingress.kubernetes.io/IamRoleArnToAssume: "arn:aws:iam::999999999999:role/alb-controller-policy-to-assume"
130-
alb.ingress.kubernetes.io/AssumeRoleExternalId: "some-magic-string"
131128
spec:
129+
iamRoleArnToAssume: "arn:aws:iam::999999999999:role/alb-controller-policy-to-assume"
130+
assumeRoleExternalId: "some-magic-string"
132131
...
133132
```
134133

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ github.com/aws/aws-sdk-go-v2/service/ec2 v1.173.0 h1:ta62lid9JkIpKZtZZXSj6rP2AqY
6060
github.com/aws/aws-sdk-go-v2/service/ec2 v1.173.0/go.mod h1:o6QDjdVKpP5EF0dp/VlvqckzuSDATr1rLdHt3A5m0YY=
6161
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.43.1 h1:L9Wt9zgtoYKIlaeFTy+EztGjL4oaXBBGtVXA+jaeYko=
6262
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.43.1/go.mod h1:yxzLdxt7bVGvIOPYIKFtiaJCJnx2ChlIIvlhW4QgI6M=
63+
github.com/aws/aws-sdk-go-v2/service/iam v1.36.3/go.mod h1:HSvujsK8xeEHMIB18oMXjSfqaN9cVqpo/MtHJIksQRk=
6364
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
6465
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
6566
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func main() {
9090
awsMetricsCollector = awsmetrics.NewCollector(metrics.Registry)
9191
}
9292

93-
cloud, err := aws.NewCloud(controllerCFG.AWSConfig, awsMetricsCollector, ctrl.Log, nil)
93+
cloud, err := aws.NewCloud(controllerCFG.AWSConfig, controllerCFG.ClusterName, awsMetricsCollector, ctrl.Log, nil)
9494
if err != nil {
9595
setupLog.Error(err, "unable to initialize AWS cloud")
9696
os.Exit(1)

pkg/aws/cloud.go

Lines changed: 45 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ package aws
33
import (
44
"context"
55
"fmt"
6-
"log"
6+
"k8s.io/apimachinery/pkg/util/cache"
77
"net"
88
"os"
9+
"regexp"
910
"strings"
11+
"sync"
12+
"time"
1013

1114
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
1215
"github.com/aws/aws-sdk-go-v2/aws/ratelimit"
@@ -32,10 +35,15 @@ import (
3235
aws_metrics "sigs.k8s.io/aws-load-balancer-controller/pkg/metrics/aws"
3336
)
3437

35-
const userAgent = "elbv2.k8s.aws"
38+
const (
39+
userAgent = "elbv2.k8s.aws"
40+
cacheTTLBufferTime = 30 * time.Second
41+
)
42+
43+
var illegalValuesInSessionName = regexp.MustCompile(`[^a-zA-Z0-9=,.@-]+`)
3644

3745
// NewCloud constructs new Cloud implementation.
38-
func NewCloud(cfg CloudConfig, metricsCollector *aws_metrics.Collector, logger logr.Logger, awsClientsProvider provider.AWSClientsProvider) (services.Cloud, error) {
46+
func NewCloud(cfg CloudConfig, clusterName string, metricsCollector *aws_metrics.Collector, logger logr.Logger, awsClientsProvider provider.AWSClientsProvider) (services.Cloud, error) {
3947
hasIPv4 := true
4048
addrs, err := net.InterfaceAddrs()
4149
if err == nil {
@@ -119,14 +127,16 @@ func NewCloud(cfg CloudConfig, metricsCollector *aws_metrics.Collector, logger l
119127

120128
thisObj := &defaultCloud{
121129
cfg: cfg,
130+
clusterName: clusterName,
122131
ec2: ec2Service,
123132
acm: services.NewACM(awsClientsProvider),
124133
wafv2: services.NewWAFv2(awsClientsProvider),
125134
wafRegional: services.NewWAFRegional(awsClientsProvider, cfg.Region),
126135
shield: services.NewShield(awsClientsProvider),
127136
rgt: services.NewRGT(awsClientsProvider),
128137

129-
assumeRoleElbV2: make(map[string]services.ELBV2),
138+
assumeRoleElbV2Cache: cache.NewExpiring(),
139+
130140
awsClientsProvider: awsClientsProvider,
131141
logger: logger,
132142
}
@@ -220,77 +230,65 @@ type defaultCloud struct {
220230
shield services.Shield
221231
rgt services.RGT
222232

223-
assumeRoleElbV2 map[string]services.ELBV2
233+
clusterName string
234+
235+
// A cache holding elbv2 clients that are assuming a role.
236+
assumeRoleElbV2Cache *cache.Expiring
237+
// assumeRoleElbV2CacheMutex protects assumeRoleElbV2Cache
238+
assumeRoleElbV2CacheMutex sync.RWMutex
239+
224240
awsClientsProvider provider.AWSClientsProvider
225241
logger logr.Logger
226242
}
227243

228-
// returns ELBV2 client for the given assumeRoleArn, or the default ELBV2 client if assumeRoleArn is empty
229-
func (c *defaultCloud) GetAssumedRoleELBV2(ctx context.Context, assumeRoleArn string, externalId string) services.ELBV2 {
230-
244+
// GetAssumedRoleELBV2 returns ELBV2 client for the given assumeRoleArn, or the default ELBV2 client if assumeRoleArn is empty
245+
func (c *defaultCloud) GetAssumedRoleELBV2(ctx context.Context, assumeRoleArn string, externalId string) (services.ELBV2, error) {
231246
if assumeRoleArn == "" {
232-
return c.elbv2
247+
return c.elbv2, nil
233248
}
234249

235-
assumedRoleELBV2, exists := c.assumeRoleElbV2[assumeRoleArn]
250+
c.assumeRoleElbV2CacheMutex.RLock()
251+
assumedRoleELBV2, exists := c.assumeRoleElbV2Cache.Get(assumeRoleArn)
252+
c.assumeRoleElbV2CacheMutex.RUnlock()
253+
236254
if exists {
237-
return assumedRoleELBV2
255+
return assumedRoleELBV2.(services.ELBV2), nil
238256
}
239257
c.logger.Info("awsCloud", "method", "GetAssumedRoleELBV2", "AssumeRoleArn", assumeRoleArn, "externalId", externalId)
240258

241-
////////////////
242259
existingAwsConfig, _ := c.awsClientsProvider.GetAWSConfig(ctx, "GetAWSConfigForIAMRoleImpersonation")
243260

244261
sourceAccount := sts.NewFromConfig(*existingAwsConfig)
245262
response, err := sourceAccount.AssumeRole(ctx, &sts.AssumeRoleInput{
246263
RoleArn: aws.String(assumeRoleArn),
247-
RoleSessionName: aws.String("aws-load-balancer-controller"),
264+
RoleSessionName: aws.String(c.makeClusterNameSessionNameSafe()),
248265
ExternalId: aws.String(externalId),
249266
})
250267
if err != nil {
251-
log.Fatalf("Unable to assume target role, %v. Attempting to use default client", err)
252-
return c.elbv2
268+
c.logger.Error(err, "Unable to assume target role, %v")
269+
return nil, err
253270
}
254271
assumedRoleCreds := response.Credentials
255272
newCreds := credentials.NewStaticCredentialsProvider(*assumedRoleCreds.AccessKeyId, *assumedRoleCreds.SecretAccessKey, *assumedRoleCreds.SessionToken)
256273
newAwsConfig, err := config.LoadDefaultConfig(ctx, config.WithRegion(c.cfg.Region), config.WithCredentialsProvider(newCreds))
257274
if err != nil {
258-
log.Fatalf("Unable to load static credentials for service client config, %v. Attempting to use default client", err)
259-
return c.elbv2
275+
c.logger.Error(err, "Unable to load static credentials for service client config, %v. Attempting to use default client")
276+
return nil, err
260277
}
261278

262-
existingAwsConfig.Credentials = newAwsConfig.Credentials // response.Credentials
279+
cacheTTL := assumedRoleCreds.Expiration.Sub(time.Now())
280+
existingAwsConfig.Credentials = newAwsConfig.Credentials
281+
elbv2WithAssumedRole := services.NewELBV2(c.awsClientsProvider, c)
263282

264-
// // var assumedRoleCreds *stsTypes.Credentials = response.Credentials
265-
266-
// // Create config with target service client, using assumed role
267-
// cfg, err = config.LoadDefaultConfig(ctx, config.WithRegion(region), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(*assumedRoleCreds.AccessKeyId, *assumedRoleCreds.SecretAccessKey, *assumedRoleCreds.SessionToken)))
268-
// if err != nil {
269-
// log.Fatalf("unable to load static credentials for service client config, %v", err)
270-
// }
271-
272-
// ////////////////
273-
// appCreds := stscreds.NewAssumeRoleProvider(client, assumeRoleArn)
274-
// value, err := appCreds.Retrieve(context.TODO())
275-
// if err != nil {
276-
// // handle error
277-
// }
278-
// /////////
279-
280-
// ///////////// OLD
281-
// creds := stscreds.NewCredentials(c.session, assumeRoleArn, func(p *stscreds.AssumeRoleProvider) {
282-
// p.ExternalID = &externalId
283-
// })
284-
// //////////////
285-
286-
// c.awsConfig.Credentials = creds
287-
// // newObj := services.NewELBV2(c.session, c, c.awsCFG)
288-
// newObj := services.NewELBV2(*c.awsConfig, c.endpointsResolver, c)
289-
290-
newObj := services.NewELBV2(c.awsClientsProvider, c)
291-
c.assumeRoleElbV2[assumeRoleArn] = newObj
283+
c.assumeRoleElbV2CacheMutex.Lock()
284+
defer c.assumeRoleElbV2CacheMutex.Unlock()
285+
c.assumeRoleElbV2Cache.Set(assumeRoleArn, elbv2WithAssumedRole, cacheTTL-cacheTTLBufferTime)
286+
return elbv2WithAssumedRole, nil
287+
}
292288

293-
return newObj
289+
func (c *defaultCloud) makeClusterNameSessionNameSafe() string {
290+
safeClusterName := illegalValuesInSessionName.ReplaceAllString(c.clusterName, "")
291+
return fmt.Sprintf("AWS-LBC-%s", safeClusterName)
294292
}
295293

296294
func (c *defaultCloud) EC2() services.EC2 {

pkg/aws/services/cloudInterface.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ type Cloud interface {
3030
// VpcID for the LoadBalancer resources.
3131
VpcID() string
3232

33-
GetAssumedRoleELBV2(ctx context.Context, assumeRoleArn string, externalId string) ELBV2
33+
GetAssumedRoleELBV2(ctx context.Context, assumeRoleArn string, externalId string) (ELBV2, error)
3434
}

pkg/aws/services/elbv2.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ type ELBV2 interface {
6060
ModifyListenerAttributesWithContext(ctx context.Context, input *elasticloadbalancingv2.ModifyListenerAttributesInput) (*elasticloadbalancingv2.ModifyListenerAttributesOutput, error)
6161
ModifyCapacityReservationWithContext(ctx context.Context, input *elasticloadbalancingv2.ModifyCapacityReservationInput) (*elasticloadbalancingv2.ModifyCapacityReservationOutput, error)
6262
DescribeCapacityReservationWithContext(ctx context.Context, input *elasticloadbalancingv2.DescribeCapacityReservationInput) (*elasticloadbalancingv2.DescribeCapacityReservationOutput, error)
63-
AssumeRole(ctx context.Context, assumeRoleArn string, externalId string) ELBV2
63+
AssumeRole(ctx context.Context, assumeRoleArn string, externalId string) (ELBV2, error)
6464
}
6565

6666
func NewELBV2(awsClientsProvider provider.AWSClientsProvider, cloud Cloud) ELBV2 {
@@ -76,9 +76,9 @@ type elbv2Client struct {
7676
cloud Cloud
7777
}
7878

79-
func (c *elbv2Client) AssumeRole(ctx context.Context, assumeRoleArn string, externalId string) ELBV2 {
79+
func (c *elbv2Client) AssumeRole(ctx context.Context, assumeRoleArn string, externalId string) (ELBV2, error) {
8080
if assumeRoleArn == "" {
81-
return c
81+
return c, nil
8282
}
8383
return c.cloud.GetAssumedRoleELBV2(ctx, assumeRoleArn, externalId)
8484
}

0 commit comments

Comments
 (0)