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},