Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,10 @@ protected ClientOverrideConfiguration.Builder createClientOverrideConfiguration(
*/
private <BuilderT extends S3BaseClientBuilder<BuilderT, ClientT>, ClientT> void configureEndpointAndRegion(
BuilderT builder, S3ClientCreationParameters parameters, Configuration conf) {
URI endpoint = getS3Endpoint(parameters.getEndpoint(), conf);
final String endpointStr = parameters.getEndpoint();
final URI endpoint = getS3Endpoint(endpointStr, conf);

String configuredRegion = parameters.getRegion();
final String configuredRegion = parameters.getRegion();
Region region = null;
String origin = "";

Expand All @@ -289,17 +290,36 @@ private <BuilderT extends S3BaseClientBuilder<BuilderT, ClientT>, ClientT> void
builder.fipsEnabled(fipsEnabled);

if (endpoint != null) {
boolean overrideEndpoint = true;
checkArgument(!fipsEnabled,
"%s : %s", ERROR_ENDPOINT_WITH_FIPS, endpoint);
builder.endpointOverride(endpoint);
// No region was configured, try to determine it from the endpoint.
if (region == null) {
region = getS3RegionFromEndpoint(parameters.getEndpoint());
boolean endpointEndsWithCentral =
endpointStr.endsWith(CENTRAL_ENDPOINT);
// No region was configured or the endpoint is central,
// determine the region from the endpoint.
if (region == null || endpointEndsWithCentral) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, not sure about this. now we're parsing region if region is null or endpoint = s3.amazonaws.com.

So if you set s3.amazonaws.com and region to eu-west-2, you still end up with us setting the region to us-east-2 and cross region enabled. My thinking here is that a lot of people may have endpoint set to s3.amazonaws.com (as atleast with SDK V1 it was harmless to do that I think) .

we only want to get into this parsing if region == null. so let's revert to the previous condition here. And then we never don't want to override if the endpoint is s3.amazonaws.com. Suggested:

    if (endpoint != null) {
      checkArgument(!fipsEnabled,
          "%s : %s", ERROR_ENDPOINT_WITH_FIPS, endpoint);
      boolean endpointEndsWithCentral =
          endpointStr.endsWith(CENTRAL_ENDPOINT);
      // No region was configured or the endpoint is central,
      // determine the region from the endpoint.
      if (region == null) {
        region = getS3RegionFromEndpoint(endpointStr,
            endpointEndsWithCentral);
        if (region != null) {
          origin = "endpoint";
          if (endpointEndsWithCentral) {
            builder.crossRegionAccessEnabled(true);
            origin = "origin with cross region access";
            LOG.debug("Enabling cross region access for endpoint {}",
                endpointStr);
          }
        }
      }
      
      // No need to override endpoint with "s3.amazonaws.com".
      // Let the client take care of endpoint resolution. Overriding
      // the endpoint with "s3.amazonaws.com" causes 400 Bad Request
      // errors for non-existent buckets and objects.
      // ref: https://github.com/aws/aws-sdk-java-v2/issues/4846
      if (!endpointEndsWithCentral) {
        builder.endpointOverride(endpoint);
        LOG.debug("Setting endpoint to {}", endpoint);
      }
    }

So now:

  1. if endpoint = s3.amazonaws.com and region is null, set to US_EAST_2 and enable cross region, and don't override endpoint.
  2. if endpoint = s3.amazonaw.com and region is set (eg to eu-west-1), set region but do not override endpoint..let SDK figure it out

@virajjasani virajjasani Jan 30, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even for user configured region, for sdk to figure out, we still need to enable cross region access right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise we will have same problem i suppose e.g. bucket on us-west-2 won't be accessible by central endpoint and us-west-1 combination. It will only be accessible by central endpoint and null region combination.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, SDK fill figure out the endpoint even if cross region is not enabled. cross region is only if you don't know the region, so we set a random region and enable it. it doesn't effect endpoint resolution behaviour afaik

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, will test this out today. Thanks a lot for the reviews!!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think anyone should set region=us-west-2 and endpoint = us-west-1 unless they like debugging things.

all we want is to handle situations where things are not set.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant "central endpoint" with "us-west-1" region, to access bucket created on us-west-2. I will test out the combination. Thanks

@virajjasani virajjasani Jan 30, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So now:

  1. if endpoint = s3.amazonaws.com and region is null, set to US_EAST_2 and enable cross region, and don't override endpoint.
  2. if endpoint = s3.amazonaw.com and region is set (eg to eu-west-1), set region but do not override endpoint..let SDK figure it out

For case#2, if user sets region to eu-west-1, is it possible that bucket might be on different region? If the answer is yes and if we want to cover that case, we will have to enable cross-region access for endpoint = s3.amazonaws.com, regardless of the region set (null or any particular one).
We should cover this, otherwise we get redirect errors, i will update the PR.

region = getS3RegionFromEndpoint(endpointStr,
endpointEndsWithCentral);
if (region != null) {
origin = "endpoint";
if (endpointEndsWithCentral) {
// No need to override endpoint with "s3.amazonaws.com".
// Let the client take care of endpoint resolution. Overriding
// the endpoint with "s3.amazonaws.com" causes 400 Bad Request
// errors for non-existent buckets and objects.
// ref: https://github.com/aws/aws-sdk-java-v2/issues/4846
overrideEndpoint = false;
builder.crossRegionAccessEnabled(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add more detail to origin, e,g 'origin with cross-region access"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

origin = "origin with cross region access";
LOG.debug("Enabling cross region access for endpoint {}",
endpointStr);
}
}
}
LOG.debug("Setting endpoint to {}", endpoint);
if (overrideEndpoint) {
builder.endpointOverride(endpoint);
LOG.debug("Setting endpoint to {}", endpoint);
}
}

if (region != null) {
Expand Down Expand Up @@ -354,20 +374,32 @@ private static URI getS3Endpoint(String endpoint, final Configuration conf) {

/**
* Parses the endpoint to get the region.
* If endpoint is the central one, use US_EAST_1.
* If endpoint is the central one, use US_EAST_2.
*
* @param endpoint the configure endpoint.
* @param endpointEndsWithCentral true if the endpoint is configured as central.
* @return the S3 region, null if unable to resolve from endpoint.
*/
private static Region getS3RegionFromEndpoint(String endpoint) {
private static Region getS3RegionFromEndpoint(final String endpoint,
final boolean endpointEndsWithCentral) {

if(!endpoint.endsWith(CENTRAL_ENDPOINT)) {
if (!endpointEndsWithCentral) {
LOG.debug("Endpoint {} is not the default; parsing", endpoint);
return AwsHostNameUtils.parseSigningRegion(endpoint, S3_SERVICE_NAME).orElse(null);
}

// endpoint is for US_EAST_1;
return Region.US_EAST_1;
// Select default region here to enable cross-region access.
Comment thread
steveloughran marked this conversation as resolved.
// If both "fs.s3a.endpoint" and "fs.s3a.endpoint.region" are empty,
// Spark sets "fs.s3a.endpoint" to "s3.amazonaws.com".
// This applies to Spark versions with the changes of SPARK-35878.
// ref:
// https://github.com/apache/spark/blob/v3.5.0/core/
// src/main/scala/org/apache/spark/deploy/SparkHadoopUtil.scala#L528
// If we do not allow cross region access, Spark would not be able to
// access any bucket that is not present in the given region.
// Hence, we should use default region us-east-2 to allow cross-region
// access.
return Region.of(AWS_S3_DEFAULT_REGION);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,14 @@
import software.amazon.awssdk.services.s3.model.HeadBucketResponse;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.contract.ContractTestUtils;
import org.apache.hadoop.fs.s3a.statistics.impl.EmptyS3AStatisticsContext;

import static org.apache.hadoop.fs.s3a.Constants.AWS_REGION;
import static org.apache.hadoop.fs.s3a.Constants.CENTRAL_ENDPOINT;
import static org.apache.hadoop.fs.s3a.Constants.ENDPOINT;
import static org.apache.hadoop.fs.s3a.Constants.PATH_STYLE_ACCESS;
import static org.apache.hadoop.fs.s3a.DefaultS3ClientFactory.ERROR_ENDPOINT_WITH_FIPS;
import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides;
Expand Down Expand Up @@ -146,7 +150,21 @@ public void testCentralEndpoint() throws Throwable {
describe("Create a client with the central endpoint");
Configuration conf = getConfiguration();

S3Client client = createS3Client(conf, CENTRAL_ENDPOINT, null, US_EAST_1, false);
S3Client client = createS3Client(conf, CENTRAL_ENDPOINT, null, US_EAST_2, false);

expectInterceptorException(client);
}

@Test
public void testCentralEndpointWithRegion() throws Throwable {
describe("Create a client with the central endpoint but also specify region");
Configuration conf = getConfiguration();

S3Client client = createS3Client(conf, CENTRAL_ENDPOINT, US_WEST_2, US_EAST_2, false);

@ahmarsuhail ahmarsuhail Jan 30, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for example here, if configured region is US_WEST_2, expected region should also be US_WEST_2, not US_EAST_2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as in #6466 I'm going to propose we make the static methods accessible and unit tests to validate them, because

  • this stuff is so important and complicated we need it running on every pr
  • everyone's ITest setup is different, so may miss things.


expectInterceptorException(client);

client = createS3Client(conf, CENTRAL_ENDPOINT, US_EAST_1, US_EAST_2, false);

expectInterceptorException(client);
}
Expand Down Expand Up @@ -257,6 +275,65 @@ public void testWithVPCE() throws Throwable {
expectInterceptorException(client);
}

@Test
Comment thread
ahmarsuhail marked this conversation as resolved.
public void testCentralEndpointCrossRegionAccess() throws Throwable {
describe("Create bucket on different region and access it using central endpoint");
final Configuration conf = getConfiguration();
removeBaseAndBucketOverrides(conf, ENDPOINT);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what should region be set to here? either unset it or explicitly set it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure will set region here because null region is anyways covered below.


final Configuration newConf = new Configuration(conf);

newConf.set(ENDPOINT, CENTRAL_ENDPOINT);

newFS = new S3AFileSystem();
newFS.initialize(getFileSystem().getUri(), newConf);

assertOpsUsingNewFs();
}

@Test
public void testCentralEndpointWithNullRegionCrossRegionAccess() throws Throwable {
describe(
"Create bucket on different region and access it using central endpoint and null region");
final Configuration conf = getConfiguration();
removeBaseAndBucketOverrides(conf, ENDPOINT, AWS_REGION);

final Configuration newConf = new Configuration(conf);

newConf.set(ENDPOINT, CENTRAL_ENDPOINT);

newFS = new S3AFileSystem();
newFS.initialize(getFileSystem().getUri(), newConf);

assertOpsUsingNewFs();
}

private void assertOpsUsingNewFs() throws IOException {
final String file = getMethodName();
Comment thread
ahmarsuhail marked this conversation as resolved.
final Path basePath = methodPath();
final Path srcDir = new Path(basePath, "srcdir");
newFS.mkdirs(srcDir);
Path srcFilePath = new Path(srcDir, file);

try (FSDataOutputStream out = newFS.create(srcFilePath)) {
out.write(new byte[] {1, 2, 3});
}

ContractTestUtils.assertIsFile(getFileSystem(), srcFilePath);
ContractTestUtils.assertIsFile(newFS, srcFilePath);

newFS.delete(srcDir, true);

Assertions
.assertThat(newFS.exists(srcFilePath))
.describedAs("Existence of file: " + srcFilePath)
.isFalse();
Assertions
.assertThat(getFileSystem().exists(srcFilePath))
.describedAs("Existence of file: " + srcFilePath)
.isFalse();
}

private final class RegionInterceptor implements ExecutionInterceptor {
private final String endpoint;
private final String region;
Expand All @@ -272,7 +349,7 @@ private final class RegionInterceptor implements ExecutionInterceptor {
public void beforeExecution(Context.BeforeExecution context,
ExecutionAttributes executionAttributes) {

if (endpoint != null) {
if (endpoint != null && !endpoint.endsWith(CENTRAL_ENDPOINT)) {
Assertions.assertThat(
executionAttributes.getAttribute(AwsExecutionAttribute.ENDPOINT_OVERRIDDEN))
.describedAs("Endpoint not overridden").isTrue();
Expand Down