diff --git a/pkg/asset/installconfig/aws/endpoints.go b/pkg/asset/installconfig/aws/endpoints.go index d9d06f5957..afed7ca832 100644 --- a/pkg/asset/installconfig/aws/endpoints.go +++ b/pkg/asset/installconfig/aws/endpoints.go @@ -28,6 +28,7 @@ const ( AwsUsGovPartitionID = "aws-us-gov" // AWS GovCloud (US) partition. AwsIsoPartitionID = "aws-iso" // AWS ISO (US) partition. AwsIsoBPartitionID = "aws-iso-b" // AWS ISOB (US) partition. + AwsEuscPartitionID = "aws-eusc" // AWS Europe Sovereign Cloud. ) var ( @@ -328,3 +329,16 @@ func GetDefaultServiceEndpoint(ctx context.Context, service string, opts Endpoin } return endpoint, nil } + +// GetPartitionIDForRegion retrieves the partition ID for a given region. +// For example, us-east-1 returns "aws" and "eusc-de-east-1" returns "aws-eusc". +func GetPartitionIDForRegion(ctx context.Context, region string) (string, error) { + // We just need to choose any services (e.g. EC2), whose version is the most up-to-date. + // If the SDK cannot resolve the endpoint for a (unknown) region, the partitionID is returned as "aws". + endpoint, err := ec2.NewDefaultEndpointResolver().ResolveEndpoint(region, ec2.EndpointResolverOptions{}) + if err != nil { + return "", fmt.Errorf("failed to resolve AWS ec2 endpoint: %w", err) + } + + return endpoint.PartitionID, nil +} diff --git a/pkg/asset/installconfig/aws/session.go b/pkg/asset/installconfig/aws/session.go index 7eea8283ed..67d5a22ae8 100644 --- a/pkg/asset/installconfig/aws/session.go +++ b/pkg/asset/installconfig/aws/session.go @@ -17,6 +17,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/sirupsen/logrus" ini "gopkg.in/ini.v1" + "k8s.io/apimachinery/pkg/util/sets" typesaws "github.com/openshift/installer/pkg/types/aws" "github.com/openshift/installer/pkg/version" @@ -28,6 +29,13 @@ var ( credentials.EnvProviderName: new(sync.Once), "credentialsFromSession": new(sync.Once), } + + // SDKv2OnlyRegions contains AWS regions that are only available in AWS SDK v2 + // and do not exist in the SDK v1 endpoint resolver. For these regions, we cannot + // use endpoints.DefaultResolver().EndpointFor() to look up signing regions as it + // would return invalid value. Instead, we use the region itself as the signing region. + // Example: eusc-de-east-1 (European Sovereign Cloud Germany East 1). + SDKv2OnlyRegions = sets.New("eusc-de-east-1") ) // SessionOptions is a function that modifies the provided session.Option. @@ -249,11 +257,15 @@ func newAWSResolver(region string, services []typesaws.ServiceEndpoint) *awsReso func (ar *awsResolver) EndpointFor(service, region string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) { if s, ok := ar.services[resolverKey(service)]; ok { logrus.Debugf("resolved AWS service %s (%s) to %q", service, region, s.URL) + signingRegion := ar.region - def, _ := endpoints.DefaultResolver().EndpointFor(service, region) - if len(def.SigningRegion) > 0 { - signingRegion = def.SigningRegion + if !SDKv2OnlyRegions.Has(ar.region) { + def, _ := endpoints.DefaultResolver().EndpointFor(service, region) //nolint:errcheck + if len(def.SigningRegion) > 0 { + signingRegion = def.SigningRegion + } } + return endpoints.ResolvedEndpoint{ URL: s.URL, SigningRegion: signingRegion, diff --git a/pkg/destroy/aws/aws.go b/pkg/destroy/aws/aws.go index 59059fe153..959c1d00e5 100644 --- a/pkg/destroy/aws/aws.go +++ b/pkg/destroy/aws/aws.go @@ -65,6 +65,7 @@ type ClusterUninstaller struct { Filters []Filter // filter(s) we will be searching for Logger logrus.FieldLogger Region string + PartitionID string ClusterID string ClusterDomain string HostedZoneRole string @@ -90,6 +91,8 @@ const ( endpointUSGovEast1 = "us-gov-east-1" endpointUSGovWest1 = "us-gov-west-1" + endpointEUSCDeEast1 = "eusc-de-east-1" + // OpenShiftInstallerDestroyerUserAgent is the User Agent key to add to the AWS Destroy API request header. OpenShiftInstallerDestroyerUserAgent = "OpenShift/4.x Destroyer" ) @@ -213,6 +216,21 @@ func New(logger logrus.FieldLogger, metadata *types.ClusterMetadata) (providers. }, nil } +// GetPartitionID returns the partition ID for the install region. +func (o *ClusterUninstaller) GetPartitionID(ctx context.Context) (string, error) { + if len(o.PartitionID) > 0 { + return o.PartitionID, nil + } + + partitionID, err := awssession.GetPartitionIDForRegion(ctx, o.Region) + if err != nil { + return "", err + } + + o.PartitionID = partitionID + return o.PartitionID, nil +} + // validate runs before the uninstall process to ensure that // all prerequisites are met for a safe destroy. func (o *ClusterUninstaller) validate(ctx context.Context) error { @@ -332,31 +350,54 @@ func (o *ClusterUninstaller) RunWithContext(ctx context.Context) ([]string, erro tagClients := []*resourcegroupstaggingapi.Client{baseTaggingClient} if o.HostedZoneRole != "" { - cfg, err := awssession.GetConfigWithOptions(ctx, configv2.WithRegion(endpointUSEast1)) - if err != nil { - return nil, fmt.Errorf("failed to create AWS config for resource tagging client: %w", err) - } + // We should use the regional STS endpoint instead of the global endpoints stsSvc, err := awssession.NewSTSClient(ctx, awssession.EndpointOptions{ - Region: endpointUSEast1, + Region: o.Region, Endpoints: o.endpoints, }, sts.WithAPIOptions(awsmiddleware.AddUserAgentKeyValue(OpenShiftInstallerDestroyerUserAgent, version.Raw))) if err != nil { return nil, fmt.Errorf("failed to create STS client: %w", err) } + partitionID, err := o.GetPartitionID(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get partition ID for region %s: %w", o.Region, err) + } + + // This tagging client is specifically for finding route53 zone, so it needs to use the "global" region, depending on the partition ID + tagRegion := o.Region + switch partitionID { + case awssession.AwsEuscPartitionID: + // For AWS EU Sovereign Cloud, use "eusc-de-east-1" + tagRegion = endpointEUSCDeEast1 + case awssession.AwsUsGovPartitionID: + // For AWS Government Cloud, use "us-gov-west-1" + tagRegion = endpointUSGovWest1 + case awssession.AwsPartitionID: + // For AWS standard, use "us-east-1" + tagRegion = endpointUSEast1 + default: + // For other partitions, use the install region + } + + cfg, err := awssession.GetConfigWithOptions(ctx, + configv2.WithRegion(tagRegion), + configv2.WithAPIOptions([]func(*middleware.Stack) error{ + awsmiddleware.AddUserAgentKeyValue(OpenShiftInstallerDestroyerUserAgent, version.Raw), + })) + if err != nil { + return nil, fmt.Errorf("failed to create AWS config for resource tagging client: %w", err) + } creds := stscreds.NewAssumeRoleProvider(stsSvc, o.HostedZoneRole) cfg.Credentials = awsv2.NewCredentialsCache(creds) - // This client is specifically for finding route53 zones, - // so it needs to use the global us-east-1 region. - tagClients = append(tagClients, createResourceTaggingClientWithConfig(cfg, endpointUSEast1, o.endpoints)) + tagClients = append(tagClients, createResourceTaggingClientWithConfig(cfg, tagRegion, o.endpoints)) } switch o.Region { + case endpointEUSCDeEast1: case endpointCNNorth1, endpointCNNorthWest1: - break case endpointISOEast1, endpointISOWest1, endpointISOBEast1: - break case endpointUSGovEast1, endpointUSGovWest1: if o.Region != endpointUSGovWest1 { tagClient, err := createResourceTaggingClient(endpointUSGovWest1, o.endpoints) diff --git a/pkg/destroy/aws/shared.go b/pkg/destroy/aws/shared.go index ab16f426f8..db9749c0ed 100644 --- a/pkg/destroy/aws/shared.go +++ b/pkg/destroy/aws/shared.go @@ -110,10 +110,14 @@ func (o *ClusterUninstaller) removeSharedTag(ctx context.Context, tagClients []* continue } + // Some regions may not support untag operations for certain resources + arns = o.filterUnsupportedUntagResources(arns) + if len(arns) == 0 { o.Logger.Debugf("No matches in %s for %s: shared, removing client", tagClient.Options().Region, key) continue } + // appending the tag client here but it needs to be removed if there is a InvalidParameterException when trying to // untag below since that only leads to an infinite loop error. nextTagClients = append(nextTagClients, tagClient) @@ -300,3 +304,38 @@ func deleteMatchingRecordSetInPublicZone(ctx context.Context, client *route53.Cl } return deleteRoute53RecordSet(ctx, client, zoneID, &matchingRecordSet, logger) } + +// filterUnsupportedUntagResources filters out ARNs that cannot be untagged due to AWS limitation. +// For example, hosted zones cannot be untagged in region "eusc-de-east-1". +func (o *ClusterUninstaller) filterUnsupportedUntagResources(arns []string) []string { + filtered := make([]string, 0, len(arns)) + skipped := make([]string, 0) + switch o.Region { + case endpointEUSCDeEast1: + for _, arnString := range arns { + parsedARN, err := arn.Parse(arnString) + if err != nil { + filtered = append(filtered, arnString) + continue + } + resourceType, _, err := splitSlash("resource", parsedARN.Resource) + if err != nil { + filtered = append(filtered, arnString) + continue + } + if parsedARN.Service == "route53" && resourceType == "hostedzone" { + skipped = append(skipped, arnString) + continue + } + filtered = append(filtered, arnString) + } + default: + filtered = arns + } + + for _, arnString := range skipped { + o.Logger.WithField("arn", arnString).Warnf("Untagging this resource via resourcetagging api is not supported by AWS in region %s. Please use the AWS Route 53 APIs, CLI, or console", o.Region) + } + + return filtered +} diff --git a/pkg/infrastructure/aws/clusterapi/iam.go b/pkg/infrastructure/aws/clusterapi/iam.go index 5f1fd242a5..82bacd67f0 100644 --- a/pkg/infrastructure/aws/clusterapi/iam.go +++ b/pkg/infrastructure/aws/clusterapi/iam.go @@ -122,6 +122,11 @@ func createIAMRoles(ctx context.Context, infraID string, ic *installconfig.Insta }) } + ec2SvcPrincipal, err := getEC2ServicePrincipal(ic.AWS.Region) + if err != nil { + return fmt.Errorf("failed to get EC2 service principal for IAM roles: %w", err) + } + assumePolicy := &iamv1.PolicyDocument{ Version: "2012-10-17", Statement: iamv1.Statements{ @@ -129,7 +134,7 @@ func createIAMRoles(ctx context.Context, infraID string, ic *installconfig.Insta Effect: "Allow", Principal: iamv1.Principals{ iamv1.PrincipalService: []string{ - getPartitionDNSSuffix(ic.AWS.Region), + ec2SvcPrincipal, }, }, Action: iamv1.Actions{ @@ -271,28 +276,31 @@ func getOrCreateIAMRole(ctx context.Context, nodeRole, infraID, assumePolicy str return *roleName, nil } -func getPartitionDNSSuffix(region string) string { +func getEC2ServicePrincipal(region string) (string, error) { endpoint, err := ec2.NewDefaultEndpointResolver().ResolveEndpoint(region, ec2.EndpointResolverOptions{}) if err != nil { - logrus.Errorf("failed to resolve AWS ec2 endpoint: %v", err) - return "" + return "", fmt.Errorf("failed to resolve AWS ec2 endpoint: %w", err) } u, err := url.Parse(endpoint.URL) if err != nil { - logrus.Errorf("failed to parse partition ID URL: %v", err) - return "" + return "", fmt.Errorf("failed to parse partition ID URL: %w", err) } domain := "amazonaws.com" - // Extract the hostname - host := u.Hostname() - // Split the hostname by "." to get the domain parts - parts := strings.Split(host, ".") - if len(parts) > 2 { - domain = strings.Join(parts[2:], ".") + switch endpoint.PartitionID { + case awsconfig.AwsEuscPartitionID: + // AWS Europe Sovereign Cloud uses ec2.amazonaws.com as service principal + default: + // Extract the hostname + host := u.Hostname() + // Split the hostname by "." to get the domain parts + parts := strings.Split(host, ".") + if len(parts) > 2 { + domain = strings.Join(parts[2:], ".") + } } - logrus.Debugf("Using domain name: %s", domain) - return fmt.Sprintf("ec2.%s", domain) + logrus.Debugf("Using domain name: %s for EC2 service principal ID", domain) + return fmt.Sprintf("ec2.%s", domain), nil }