From 08ad0f86176b0f879f4bd4686d234a8d1fa5c57a Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Thu, 22 Jan 2026 17:10:09 -0800 Subject: [PATCH] CORS-4055, CORS-4078: migrate ELB/ELBv2 API calls to AWS SDK v2 The commit is an incremental step to migrate AWS API calls to AWS SDK v2. This focuses on ELB/ELBv2 clients in the pkg/asset and dependent pkg(s). The ELB and ELBv2 clients now use SDK v2 with custom endpoint resolvers that maintain backwards compatibility with SDK v1 service endpoint configurations. Special handling is included for the fact that SDK v1 used the same endpoint identifier ("elasticloadbalancing") for both ELB classic and ELBv2, while SDK v2 uses distinct service IDs. --- pkg/asset/installconfig/aws/clients.go | 40 +++++++++++ pkg/asset/installconfig/aws/endpoints.go | 92 ++++++++++++++++++++---- pkg/infrastructure/aws/clusterapi/aws.go | 17 ++--- 3 files changed, 123 insertions(+), 26 deletions(-) diff --git a/pkg/asset/installconfig/aws/clients.go b/pkg/asset/installconfig/aws/clients.go index 92a3aa0ec2..558d27c49b 100644 --- a/pkg/asset/installconfig/aws/clients.go +++ b/pkg/asset/installconfig/aws/clients.go @@ -7,6 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/ec2" + elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" + elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -33,6 +35,44 @@ func NewEC2Client(ctx context.Context, endpointOpts EndpointOptions, optFns ...f return ec2.NewFromConfig(cfg, ec2Opts...), nil } +// NewELBClient creates a new ELB (classic) client. +func NewELBClient(ctx context.Context, endpointOpts EndpointOptions, optFns ...func(*elb.Options)) (*elb.Client, error) { + cfg, err := GetConfigWithOptions(ctx, config.WithRegion(endpointOpts.Region)) + if err != nil { + return nil, err + } + + elbOpts := []func(*elb.Options){ + func(o *elb.Options) { + o.EndpointResolverV2 = &ELBEndpointResolver{ + ServiceEndpointResolver: NewServiceEndpointResolver(endpointOpts), + } + }, + } + elbOpts = append(elbOpts, optFns...) + + return elb.NewFromConfig(cfg, elbOpts...), nil +} + +// NewELBV2Client creates a new ELBV2 client. +func NewELBV2Client(ctx context.Context, endpointOpts EndpointOptions, optFns ...func(*elbv2.Options)) (*elbv2.Client, error) { + cfg, err := GetConfigWithOptions(ctx, config.WithRegion(endpointOpts.Region)) + if err != nil { + return nil, err + } + + elbv2Opts := []func(*elbv2.Options){ + func(o *elbv2.Options) { + o.EndpointResolverV2 = &ELBV2EndpointResolver{ + ServiceEndpointResolver: NewServiceEndpointResolver(endpointOpts), + } + }, + } + elbv2Opts = append(elbv2Opts, optFns...) + + return elbv2.NewFromConfig(cfg, elbv2Opts...), nil +} + // NewIAMClient creates a new IAM API client. func NewIAMClient(ctx context.Context, endpointOpts EndpointOptions, optFns ...func(*iam.Options)) (*iam.Client, error) { cfg, err := GetConfigWithOptions(ctx, config.WithRegion(endpointOpts.Region)) diff --git a/pkg/asset/installconfig/aws/endpoints.go b/pkg/asset/installconfig/aws/endpoints.go index 5cadd280d2..a566892c48 100644 --- a/pkg/asset/installconfig/aws/endpoints.go +++ b/pkg/asset/installconfig/aws/endpoints.go @@ -3,6 +3,7 @@ package aws import ( "context" "fmt" + "sync" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ec2" @@ -30,24 +31,28 @@ const ( ) var ( - // v1Tov2ServiceIDMap maps v1 service ID to its v2 equivalent. - v1Tov2ServiceIDMap = map[string]string{ - "ec2": ec2.ServiceID, - "elasticloadbalancing": elb.ServiceID, - "elasticloadbalancingv2": elbv2.ServiceID, - "iam": iam.ServiceID, - "route53": route53.ServiceID, - "s3": s3.ServiceID, - "sts": sts.ServiceID, - "resourcegroupstaggingapi": resourcegroupstaggingapi.ServiceID, - "servicequotas": servicequotas.ServiceID, + // In v1 sdk, a constant EndpointsID is exported in each service to look up the custom service endpoint. + // For example: https://github.com/aws/aws-sdk-go/blob/070853e88d22854d2355c2543d0958a5f76ad407/service/resourcegroupstaggingapi/service.go#L33-L34 + // In v2 SDK, these constants are no longer available. + // For backwards compatibility, we copy those constants from the SDK v1 and map it to ServiceID in SDK v2. + compatServiceIDMap = map[string]string{ + "ec2": ec2.ServiceID, + "elasticloadbalancing": elb.ServiceID, + "iam": iam.ServiceID, + "route53": route53.ServiceID, + "s3": s3.ServiceID, + "sts": sts.ServiceID, + "tagging": resourcegroupstaggingapi.ServiceID, + "servicequotas": servicequotas.ServiceID, } + // logELBv2FallbackOnce logs the ELBv2 fallback once. + logELBv2FallbackOnce sync.Once ) -// resolveServiceID converts a service ID in the SDK from v1 to v2. -// If the service ID is not recognized, return as-is. +// resolveServiceID returns the serviceID for service endpoint resolvers to look up the endpoint URL. +// If the serviceID is an SDKv1 identifier, this converts it SDKv2. Otherwise, return as-is. func resolveServiceID(serviceID string) string { - if v2serviceID, ok := v1Tov2ServiceIDMap[serviceID]; ok { + if v2serviceID, ok := compatServiceIDMap[serviceID]; ok { return v2serviceID } return serviceID @@ -76,6 +81,23 @@ func NewServiceEndpointResolver(opts EndpointOptions) *ServiceEndpointResolver { for _, endpoint := range opts.Endpoints { endpointMap[resolveServiceID(endpoint.Name)] = endpoint } + + // In v1 SDK, elb and elbv2 uses the same identifier, thus the same endpoint. + // elbv2: https://github.com/aws/aws-sdk-go/blob/070853e88d22854d2355c2543d0958a5f76ad407/service/elbv2/service.go#L32-L33 + // elb: https://github.com/aws/aws-sdk-go/blob/070853e88d22854d2355c2543d0958a5f76ad407/service/elb/service.go#L32-L33 + // For backwards compatibility, if elbv2 endpoint is undefined, the elbv2 endpoint resolver should fall back to elb endpoint if any. + if _, ok := endpointMap[elbv2.ServiceID]; !ok { + if elbEp, ok := endpointMap[elb.ServiceID]; ok { + logELBv2FallbackOnce.Do(func() { + logrus.Infof("elbv2 endpoint is empty, using elb endpoint: %s", elbEp.URL) + }) + endpointMap[elbv2.ServiceID] = typesaws.ServiceEndpoint{ + Name: elbv2.ServiceID, + URL: elbEp.URL, + } + } + } + return &ServiceEndpointResolver{ endpoints: endpointMap, endpointOptions: opts, @@ -103,6 +125,48 @@ func (s *EC2EndpointResolver) ResolveEndpoint(ctx context.Context, params ec2.En return ec2.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params) } +// ELBEndpointResolver implements EndpointResolverV2 interface for ELB (classic). +type ELBEndpointResolver struct { + *ServiceEndpointResolver +} + +// ResolveEndpoint for ELB. +func (s *ELBEndpointResolver) ResolveEndpoint(ctx context.Context, params elb.EndpointParameters) (smithyendpoints.Endpoint, error) { + params.UseDualStack = aws.Bool(s.endpointOptions.UseDualStack) + params.UseFIPS = aws.Bool(s.endpointOptions.UseFIPS) + + // If custom endpoint not found, return default endpoint for the service. + endpoint, ok := s.endpoints[elb.ServiceID] + if !ok { + return elb.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params) + } + + params.Endpoint = aws.String(endpoint.URL) + params.Region = aws.String(s.endpointOptions.Region) + return elb.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params) +} + +// ELBV2EndpointResolver implements EndpointResolverV2 interface for ELBV2. +type ELBV2EndpointResolver struct { + *ServiceEndpointResolver +} + +// ResolveEndpoint for ELBV2. +func (s *ELBV2EndpointResolver) ResolveEndpoint(ctx context.Context, params elbv2.EndpointParameters) (smithyendpoints.Endpoint, error) { + params.UseDualStack = aws.Bool(s.endpointOptions.UseDualStack) + params.UseFIPS = aws.Bool(s.endpointOptions.UseFIPS) + + // If custom endpoint not found, return default endpoint for the service. + endpoint, ok := s.endpoints[elbv2.ServiceID] + if !ok { + return elbv2.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params) + } + + params.Endpoint = aws.String(endpoint.URL) + params.Region = aws.String(s.endpointOptions.Region) + return elbv2.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params) +} + // IAMEndpointResolver implements EndpointResolverV2 interface for IAM. type IAMEndpointResolver struct { *ServiceEndpointResolver diff --git a/pkg/infrastructure/aws/clusterapi/aws.go b/pkg/infrastructure/aws/clusterapi/aws.go index 2c5d02ac0d..8d09c80d84 100644 --- a/pkg/infrastructure/aws/clusterapi/aws.go +++ b/pkg/infrastructure/aws/clusterapi/aws.go @@ -8,7 +8,6 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" - configv2 "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ec2" elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" @@ -275,20 +274,14 @@ func getHostedZoneIDForNLB(ctx context.Context, ic *installconfig.InstallConfig, return hzID, nil } - cfg, err := configv2.LoadDefaultConfig(ctx, configv2.WithRegion(ic.Config.Platform.AWS.Region)) + client, err := awsconfig.NewELBV2Client(ctx, awsconfig.EndpointOptions{ + Region: ic.Config.AWS.Region, + Endpoints: ic.Config.AWS.ServiceEndpoints, + }) if err != nil { - return "", fmt.Errorf("failed to load AWS config: %w", err) + return "", fmt.Errorf("failed to create elbv2 client: %w", err) } - client := elbv2.NewFromConfig(cfg, func(options *elbv2.Options) { - options.Region = ic.Config.Platform.AWS.Region - for _, endpoint := range ic.Config.AWS.ServiceEndpoints { - if strings.EqualFold(endpoint.Name, "elasticloadbalancing") { - options.BaseEndpoint = aws.String(endpoint.URL) - } - } - }) - // If the HostedZoneID is not known, query from the LoadBalancer input := elbv2.DescribeLoadBalancersInput{ Names: []string{lbName},