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
13 changes: 13 additions & 0 deletions pkg/asset/installconfig/aws/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
43 changes: 39 additions & 4 deletions pkg/destroy/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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{
Expand Down
42 changes: 42 additions & 0 deletions pkg/destroy/aws/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
37 changes: 23 additions & 14 deletions pkg/infrastructure/aws/clusterapi/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -122,14 +123,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 +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
}
24 changes: 24 additions & 0 deletions pkg/types/aws/defaults/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
38 changes: 38 additions & 0 deletions pkg/types/aws/defaults/platform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,51 @@ 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 {
t.Run(tc.name, func(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)
})
}
}
6 changes: 6 additions & 0 deletions pkg/types/aws/regions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).
)