Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions pkg/asset/installconfig/aws/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
}
18 changes: 15 additions & 3 deletions pkg/asset/installconfig/aws/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
61 changes: 51 additions & 10 deletions pkg/destroy/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions pkg/destroy/aws/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
36 changes: 22 additions & 14 deletions pkg/infrastructure/aws/clusterapi/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,19 @@ 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{
{
Effect: "Allow",
Principal: iamv1.Principals{
iamv1.PrincipalService: []string{
getPartitionDNSSuffix(ic.AWS.Region),
ec2SvcPrincipal,
},
},
Action: iamv1.Actions{
Expand Down Expand Up @@ -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
}