diff --git a/pkg/asset/installconfig/aws/endpoints.go b/pkg/asset/installconfig/aws/endpoints.go index ba5c85621b1..700d0bee6ab 100644 --- a/pkg/asset/installconfig/aws/endpoints.go +++ b/pkg/asset/installconfig/aws/endpoints.go @@ -363,3 +363,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 endpoint for region %s get partition ID: %w", region, err) + } + + return endpoint.PartitionID, nil +} diff --git a/pkg/destroy/aws/aws.go b/pkg/destroy/aws/aws.go index 845817801e1..ba18c43dd0f 100644 --- a/pkg/destroy/aws/aws.go +++ b/pkg/destroy/aws/aws.go @@ -61,6 +61,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 @@ -150,6 +151,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 { @@ -250,10 +266,30 @@ func (o *ClusterUninstaller) RunWithContext(ctx context.Context) ([]string, erro tagClients := []*resourcegroupstaggingapi.Client{baseTaggingClient} if o.HostedZoneRole != "" { + 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 awstypes.AwsEuscPartitionID: + // For AWS EU Sovereign Cloud, use "eusc-de-east-1" + tagRegion = awstypes.EuscDeEast1RegionID + case awstypes.AwsUsGovPartitionID: + // For AWS Government Cloud, use "us-gov-west-1" + tagRegion = awstypes.UsGovWest1RegionID + case awstypes.AwsPartitionID: + // For AWS standard, use "us-east-1" + tagRegion = awstypes.UsEast1RegionID + default: + // For other partitions, use the install region + } + // Create tagging client with assumed role credentials for cross-account hosted zone access. - // This client is specifically for finding route53 zones in the us-east-1 region. assumedRoleTagClient, err := awssession.NewResourceGroupsTaggingAPIClient(ctx, awssession.EndpointOptions{ - Region: awstypes.UsEast1RegionID, + Region: tagRegion, Endpoints: o.endpoints, }, o.HostedZoneRole, resourcegroupstaggingapi.WithAPIOptions(awsmiddleware.AddUserAgentKeyValue(awssession.OpenShiftInstallerDestroyerUserAgent, version.Raw))) if err != nil { @@ -263,10 +299,9 @@ func (o *ClusterUninstaller) RunWithContext(ctx context.Context) ([]string, erro } switch o.Region { + case awstypes.EuscDeEast1RegionID: case awstypes.CnNorth1RegionID, awstypes.CnNorthwest1RegionID: - break case awstypes.UsIsoEast1RegionID, awstypes.UsIsoWest1RegionID, awstypes.UsIsoBEast1RegionID: - break case awstypes.UsGovEast1RegionID, awstypes.UsGovWest1RegionID: if o.Region != awstypes.UsGovWest1RegionID { tagClient, err := awssession.NewResourceGroupsTaggingAPIClient(ctx, awssession.EndpointOptions{ diff --git a/pkg/destroy/aws/shared.go b/pkg/destroy/aws/shared.go index a7968acd4f7..9d856b4b0bd 100644 --- a/pkg/destroy/aws/shared.go +++ b/pkg/destroy/aws/shared.go @@ -19,6 +19,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" awssession "github.com/openshift/installer/pkg/asset/installconfig/aws" + awstypes "github.com/openshift/installer/pkg/types/aws" "github.com/openshift/installer/pkg/version" ) @@ -110,10 +111,14 @@ func (o *ClusterUninstaller) removeSharedTag(ctx context.Context, tagClients []* continue } + // Some regions may not support untag operations for certain resources + arns = o.filterUnsupportedUntagResources(tagClient.Options().Region, 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 +305,40 @@ 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". +// FIXME: remove this handler once AWS added support for untagging hosted zone via resourcetagging API for EU Sovereign Cloud. +func (o *ClusterUninstaller) filterUnsupportedUntagResources(region string, arns []string) []string { + filtered := make([]string, 0, len(arns)) + skipped := make([]string, 0) + + switch region { + case awstypes.EuscDeEast1RegionID: + 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", region) + } + + return filtered +} diff --git a/pkg/infrastructure/aws/clusterapi/iam.go b/pkg/infrastructure/aws/clusterapi/iam.go index 5f1fd242a56..f5b2617f03e 100644 --- a/pkg/infrastructure/aws/clusterapi/iam.go +++ b/pkg/infrastructure/aws/clusterapi/iam.go @@ -18,6 +18,7 @@ import ( "github.com/openshift/installer/pkg/asset/installconfig" awsconfig "github.com/openshift/installer/pkg/asset/installconfig/aws" + awstypes "github.com/openshift/installer/pkg/types/aws" ) const ( @@ -122,6 +123,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 +135,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 +277,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 awstypes.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 } diff --git a/pkg/types/aws/defaults/platform.go b/pkg/types/aws/defaults/platform.go index 038d190cf66..f011c0841e1 100644 --- a/pkg/types/aws/defaults/platform.go +++ b/pkg/types/aws/defaults/platform.go @@ -32,6 +32,22 @@ var ( // - us-east-1e is a well-known limited zone, with limited offering of // instance types supported by installer. skippedZones = []string{"us-east-1e"} + + // defaultServiceEndpoints is a list of known default endpoints for specific regions that would + // otherwise require user to set the service overrides. + // Note: This is a workaround for when the AWS SDK cannot yet handle new regions, for example, EUSC regions. + defaultServiceEndpoints = map[string][]aws.ServiceEndpoint{ + // Reference: https://docs.aws.eu/general/latest/gr/endpoints.html + aws.EuscDeEast1RegionID: { + {Name: "ec2", URL: "https://ec2.eusc-de-east-1.amazonaws.eu"}, + {Name: "elasticloadbalancing", URL: "https://elasticloadbalancing.eusc-de-east-1.amazonaws.eu"}, + {Name: "s3", URL: "https://s3.eusc-de-east-1.amazonaws.eu"}, + {Name: "route53", URL: "https://route53.amazonaws.eu"}, + {Name: "iam", URL: "https://iam.eusc-de-east-1.amazonaws.eu"}, + {Name: "sts", URL: "https://sts.eusc-de-east-1.amazonaws.eu"}, + {Name: "tagging", URL: "https://tagging.eusc-de-east-1.amazonaws.eu"}, + }, + } ) // SetPlatformDefaults sets the defaults for the platform. @@ -46,6 +62,14 @@ func SetPlatformDefaults(p *aws.Platform) { p.LBType = configv1.NLB } } + + // TODO: Remove when all openshift components migrate to AWS SDK v2 + if len(p.ServiceEndpoints) == 0 { + if eps, ok := defaultServiceEndpoints[p.Region]; ok { + p.ServiceEndpoints = eps + logrus.Infof("Adding default service endpoints for region %s", p.Region) + } + } } // InstanceTypes returns a list of instance types, in decreasing priority order, which we should use for a given diff --git a/pkg/types/aws/defaults/platform_test.go b/pkg/types/aws/defaults/platform_test.go index 14154d80476..243fb22769c 100644 --- a/pkg/types/aws/defaults/platform_test.go +++ b/pkg/types/aws/defaults/platform_test.go @@ -141,6 +141,43 @@ func TestSetPlatformDefaults(t *testing.T) { LBType: "", }, }, + { + name: "EUSC region should set default service endpoints", + platform: &aws.Platform{ + Region: aws.EuscDeEast1RegionID, + }, + expected: &aws.Platform{ + Region: aws.EuscDeEast1RegionID, + IPFamily: network.IPv4, + ServiceEndpoints: defaultServiceEndpoints[aws.EuscDeEast1RegionID], + }, + }, + { + name: "non-EUSC region should not set default service endpoints", + platform: &aws.Platform{ + Region: "us-east-1", + }, + expected: &aws.Platform{ + Region: "us-east-1", + IPFamily: network.IPv4, + }, + }, + { + name: "EUSC region with existing service endpoints should not set defaults", + platform: &aws.Platform{ + Region: aws.EuscDeEast1RegionID, + ServiceEndpoints: []aws.ServiceEndpoint{ + {Name: "ec2", URL: "https://custom.ec2.endpoint"}, + }, + }, + expected: &aws.Platform{ + Region: aws.EuscDeEast1RegionID, + IPFamily: network.IPv4, + ServiceEndpoints: []aws.ServiceEndpoint{ + {Name: "ec2", URL: "https://custom.ec2.endpoint"}, + }, + }, + }, } for _, tc := range cases { @@ -148,6 +185,7 @@ func TestSetPlatformDefaults(t *testing.T) { SetPlatformDefaults(tc.platform) assert.Equal(t, tc.expected.IPFamily, tc.platform.IPFamily) assert.Equal(t, tc.expected.LBType, tc.platform.LBType) + assert.Equal(t, tc.expected.ServiceEndpoints, tc.platform.ServiceEndpoints) }) } } diff --git a/pkg/types/aws/regions.go b/pkg/types/aws/regions.go index 28e730e03e5..6866f40eda5 100644 --- a/pkg/types/aws/regions.go +++ b/pkg/types/aws/regions.go @@ -88,6 +88,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. ) // AWS Standard partition's regions. @@ -150,3 +151,8 @@ const ( const ( UsIsoBEast1RegionID = "us-isob-east-1" // AWS ISOB (US) East. ) + +// AWS European Sovereign Cloud partition's regions. +const ( + EuscDeEast1RegionID = "eusc-de-east-1" // AWS European Sovereign Cloud (Germany). +)