diff --git a/docs/content.zh/docs/deployment/filesystems/s3.md b/docs/content.zh/docs/deployment/filesystems/s3.md index f8f7bad7a8e8e9..68e7ddb2adab7c 100644 --- a/docs/content.zh/docs/deployment/filesystems/s3.md +++ b/docs/content.zh/docs/deployment/filesystems/s3.md @@ -220,6 +220,33 @@ cp ./opt/flink-s3-fs-hadoop-{{< version >}}.jar ./plugins/s3-fs-hadoop/ The [common configuration](#common-configuration) options apply. In addition, [Hadoop's s3a configuration keys](https://hadoop.apache.org/docs/stable/hadoop-aws/tools/hadoop-aws/index.html#S3A) are supported. Hadoop configuration keys are automatically translated — for example, `fs.s3a.connection.maximum` becomes `s3.connection.maximum`. +**Input stream type:** hadoop-aws 3.4.3 changes its default input stream to the [S3 Analytics Accelerator](https://hadoop.apache.org/docs/stable/hadoop-aws/tools/hadoop-aws/stream_architecture.html) (`fs.s3a.input.stream.type: analytics`), which alters read patterns, per-stream memory usage, and the volume of S3 GET requests. To keep upgrades behavior-preserving, Flink pins the classic input stream by default. To opt in to the analytics stream, set `s3.input.stream.type: analytics`; setting the legacy `s3.prefetch.enabled: true` toggle is also respected and selects Hadoop's prefetching stream. + +#### Changed defaults from the Hadoop 3.4 upgrade + +The Hadoop S3 FileSystem passes Hadoop's s3a defaults through unchanged. Hadoop 3.4 retuned several of them as part of its migration to the AWS SDK v2; Flink deliberately keeps the new values (the input stream type above is the only pinned exception), so that the client behaves the way the Hadoop community tunes and tests it. The table below lists the changed defaults and why upstream changed them. Any key can be restored through the `s3.` passthrough shown underneath. + +| Key | Hadoop 3.3 default | Hadoop 3.4 default | Why it changed | +|-----|--------------------|--------------------|----------------| +| `fs.s3a.connection.maximum` | 96 | 500 | HTTP connection pool resized for the AWS SDK v2 client and the larger thread pool, avoiding pool starvation under parallel uploads. | +| `fs.s3a.threads.max` | 10 | 96 | Sized to match the larger connection pool for parallel upload and copy operations. | +| `fs.s3a.attempts.maximum` | 10 | 5 | Retries now use the SDK v2 adaptive retry strategy (client-side throttling-aware rate limiting), so fewer, smarter attempts replace many blind ones. | +| `fs.s3a.connection.establish.timeout` | 50s | 30s | Fail over to healthy endpoints sooner. | +| `fs.s3a.connection.acquisition.timeout` | n/a | 60s | New in 3.4: bounds the wait for a pooled connection under load instead of blocking indefinitely. | +| `fs.s3a.connection.ttl` | n/a | 5m | New in 3.4: recycles pooled connections so load-balancer and DNS changes are picked up. | +| `fs.s3a.directory.marker.retention` | `delete` | `keep` | Keeping zero-byte directory markers saves one DELETE request per object created under a path and avoids delete tombstones on versioned buckets. Markers are invisible through the filesystem API; restore `delete` only if the bucket is shared with Hadoop clients older than 3.1 or with tools that list raw objects. | +| `fs.s3a.checksum.generation` | n/a | `false` | New in 3.4: disabled by default to stay compatible with S3-compatible stores that reject the SDK v2 default integrity checksums. | +| `fs.s3a.input.stream.type` | `classic` | `analytics` upstream, **Flink pins `classic`** | See the input stream type note above. | + +To restore the previous value of any of these keys (normally unnecessary), configure the corresponding `s3.` key, for example: + +```yaml +s3.connection.maximum: 96 +s3.threads.max: 10 +s3.attempts.maximum: 10 +s3.directory.marker.retention: delete +``` + --- ## Using Multiple S3 Implementations diff --git a/docs/content/docs/deployment/filesystems/s3.md b/docs/content/docs/deployment/filesystems/s3.md index 36c2b55bad31e3..d24aa3ddf8804b 100644 --- a/docs/content/docs/deployment/filesystems/s3.md +++ b/docs/content/docs/deployment/filesystems/s3.md @@ -228,6 +228,33 @@ cp ./opt/flink-s3-fs-hadoop-{{< version >}}.jar ./plugins/s3-fs-hadoop/ The [common configuration](#common-configuration) options apply. In addition, [Hadoop's s3a configuration keys](https://hadoop.apache.org/docs/stable/hadoop-aws/tools/hadoop-aws/index.html#S3A) are supported. Hadoop configuration keys are automatically translated — for example, `fs.s3a.connection.maximum` becomes `s3.connection.maximum`. +**Input stream type:** hadoop-aws 3.4.3 changes its default input stream to the [S3 Analytics Accelerator](https://hadoop.apache.org/docs/stable/hadoop-aws/tools/hadoop-aws/stream_architecture.html) (`fs.s3a.input.stream.type: analytics`), which alters read patterns, per-stream memory usage, and the volume of S3 GET requests. To keep upgrades behavior-preserving, Flink pins the classic input stream by default. To opt in to the analytics stream, set `s3.input.stream.type: analytics`; setting the legacy `s3.prefetch.enabled: true` toggle is also respected and selects Hadoop's prefetching stream. + +#### Changed defaults from the Hadoop 3.4 upgrade + +The Hadoop S3 FileSystem passes Hadoop's s3a defaults through unchanged. Hadoop 3.4 retuned several of them as part of its migration to the AWS SDK v2; Flink deliberately keeps the new values (the input stream type above is the only pinned exception), so that the client behaves the way the Hadoop community tunes and tests it. The table below lists the changed defaults and why upstream changed them. Any key can be restored through the `s3.` passthrough shown underneath. + +| Key | Hadoop 3.3 default | Hadoop 3.4 default | Why it changed | +|-----|--------------------|--------------------|----------------| +| `fs.s3a.connection.maximum` | 96 | 500 | HTTP connection pool resized for the AWS SDK v2 client and the larger thread pool, avoiding pool starvation under parallel uploads. | +| `fs.s3a.threads.max` | 10 | 96 | Sized to match the larger connection pool for parallel upload and copy operations. | +| `fs.s3a.attempts.maximum` | 10 | 5 | Retries now use the SDK v2 adaptive retry strategy (client-side throttling-aware rate limiting), so fewer, smarter attempts replace many blind ones. | +| `fs.s3a.connection.establish.timeout` | 50s | 30s | Fail over to healthy endpoints sooner. | +| `fs.s3a.connection.acquisition.timeout` | n/a | 60s | New in 3.4: bounds the wait for a pooled connection under load instead of blocking indefinitely. | +| `fs.s3a.connection.ttl` | n/a | 5m | New in 3.4: recycles pooled connections so load-balancer and DNS changes are picked up. | +| `fs.s3a.directory.marker.retention` | `delete` | `keep` | Keeping zero-byte directory markers saves one DELETE request per object created under a path and avoids delete tombstones on versioned buckets. Markers are invisible through the filesystem API; restore `delete` only if the bucket is shared with Hadoop clients older than 3.1 or with tools that list raw objects. | +| `fs.s3a.checksum.generation` | n/a | `false` | New in 3.4: disabled by default to stay compatible with S3-compatible stores that reject the SDK v2 default integrity checksums. | +| `fs.s3a.input.stream.type` | `classic` | `analytics` upstream, **Flink pins `classic`** | See the input stream type note above. | + +To restore the previous value of any of these keys (normally unnecessary), configure the corresponding `s3.` key, for example: + +```yaml +s3.connection.maximum: 96 +s3.threads.max: 10 +s3.attempts.maximum: 10 +s3.directory.marker.retention: delete +``` + --- ## Using Multiple S3 Implementations diff --git a/flink-filesystems/flink-azure-fs-hadoop/src/main/resources/META-INF/NOTICE b/flink-filesystems/flink-azure-fs-hadoop/src/main/resources/META-INF/NOTICE index c90779b76eefa7..92739f45555185 100644 --- a/flink-filesystems/flink-azure-fs-hadoop/src/main/resources/META-INF/NOTICE +++ b/flink-filesystems/flink-azure-fs-hadoop/src/main/resources/META-INF/NOTICE @@ -6,19 +6,15 @@ The Apache Software Foundation (http://www.apache.org/). This project bundles the following dependencies under the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt) -- com.fasterxml.jackson.core:jackson-core:2.22.2 -- com.google.guava:guava:20.0 - commons-codec:commons-codec:1.15 - commons-logging:commons-logging:1.1.3 -- org.apache.hadoop.thirdparty:hadoop-shaded-guava:1.1.1 -- org.apache.hadoop:hadoop-azure:3.3.4 +- org.apache.hadoop.thirdparty:hadoop-shaded-guava:1.5.0 +- org.apache.hadoop:hadoop-azure:3.4.3 - org.apache.httpcomponents:httpclient:4.5.13 - org.apache.httpcomponents:httpcore:4.4.14 -- org.codehaus.jackson:jackson-core-asl:1.9.13 -- org.codehaus.jackson:jackson-mapper-asl:1.9.14.jdk17-redhat-00001 - org.eclipse.jetty:jetty-util-ajax:9.3.24.v20180605 - org.eclipse.jetty:jetty-util:9.3.24.v20180605 -- org.wildfly.openssl:wildfly-openssl:1.0.7.Final +- org.wildfly.openssl:wildfly-openssl:2.2.5.Final This project bundles the following dependencies under the MIT (https://opensource.org/licenses/MIT) @@ -29,4 +25,4 @@ The bundled Apache Hadoop Relocated (Shaded) Third-party Miscellaneous Libs org.apache.hadoop.thirdparty:hadoop-shaded-guava dependency bundles the following dependencies under the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt) -- com.google.guava:guava:30.1.1-jre +- com.google.guava:guava:33.4.8-jre diff --git a/flink-filesystems/flink-fs-hadoop-shaded/pom.xml b/flink-filesystems/flink-fs-hadoop-shaded/pom.xml index 8352d69b03f61b..7a0cc85a1545e5 100644 --- a/flink-filesystems/flink-fs-hadoop-shaded/pom.xml +++ b/flink-filesystems/flink-fs-hadoop-shaded/pom.xml @@ -43,6 +43,12 @@ under the License. commons-beanutils 1.9.4 + + + io.dropwizard.metrics + metrics-core + 3.2.5 + @@ -78,6 +84,19 @@ under the License. com.sun.jersey jersey-server + + + com.github.pjfanning + jersey-json + + + + com.sun.xml.bind + jaxb-impl + org.apache.avro avro @@ -178,6 +197,21 @@ under the License. org.slf4j slf4j-reload4j + + + io.netty + netty-handler + + + io.netty + netty-transport-native-epoll + @@ -296,13 +330,6 @@ under the License. - - - - true - - - diff --git a/flink-filesystems/flink-fs-hadoop-shaded/src/main/resources/META-INF/NOTICE b/flink-filesystems/flink-fs-hadoop-shaded/src/main/resources/META-INF/NOTICE index ad0f66bc5bb7fd..41c96ce623b2c5 100644 --- a/flink-filesystems/flink-fs-hadoop-shaded/src/main/resources/META-INF/NOTICE +++ b/flink-filesystems/flink-fs-hadoop-shaded/src/main/resources/META-INF/NOTICE @@ -9,39 +9,46 @@ This project bundles the following dependencies under the Apache Software Licens - com.fasterxml.jackson.core:jackson-annotations:2.22 - com.fasterxml.jackson.core:jackson-core:2.22.2 - com.fasterxml.jackson.core:jackson-databind:2.22.2 -- com.fasterxml.woodstox:woodstox-core:5.3.0 -- com.google.guava:failureaccess:1.0 -- com.google.guava:guava:27.0-jre +- com.fasterxml.woodstox:woodstox-core:5.4.0 +- com.google.guava:failureaccess:1.0.1 +- com.google.guava:guava:32.0.1-jre - com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava -- com.google.j2objc:j2objc-annotations:1.1 -- commons-beanutils:commons-beanutils:1.9.4 -- commons-collections:commons-collections:3.2.2 +- com.google.j2objc:j2objc-annotations:2.8 - commons-io:commons-io:2.15.1 - commons-logging:commons-logging:1.1.3 +- io.dropwizard.metrics:metrics-core:3.2.5 +- org.apache.commons:commons-collections4:4.4 - org.apache.commons:commons-compress:1.26.0 -- org.apache.commons:commons-configuration2:2.1.1 +- org.apache.commons:commons-configuration2:2.10.1 - org.apache.commons:commons-lang3:3.18.0 - org.apache.commons:commons-text:1.10.0 -- org.apache.hadoop.thirdparty:hadoop-shaded-guava:1.1.1 -- org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_7:1.1.1 -- org.apache.hadoop:hadoop-annotations:3.3.4 -- org.apache.hadoop:hadoop-auth:3.3.4 -- org.apache.hadoop:hadoop-common:3.3.4 -- org.apache.kerby:kerb-core:1.0.1 -- org.apache.kerby:kerby-asn1:1.0.1 -- org.apache.kerby:kerby-pkix:1.0.1 -- org.apache.kerby:kerby-util:1.0.1 +- org.apache.hadoop.thirdparty:hadoop-shaded-guava:1.5.0 +- org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_25:1.5.0 +- org.apache.hadoop:hadoop-annotations:3.4.3 +- org.apache.hadoop:hadoop-auth:3.4.3 +- org.apache.hadoop:hadoop-common:3.4.3 +- org.apache.kerby:kerb-core:2.0.3 +- org.apache.kerby:kerb-crypto:2.0.3 +- org.apache.kerby:kerb-util:2.0.3 +- org.apache.kerby:kerby-asn1:2.0.3 +- org.apache.kerby:kerby-config:2.0.3 +- org.apache.kerby:kerby-pkix:2.0.3 +- org.apache.kerby:kerby-util:2.0.3 +- org.codehaus.jettison:jettison:1.5.4 - org.xerial.snappy:snappy-java:1.1.10.7 This project bundles the following dependencies under the MIT (https://opensource.org/licenses/MIT) -- org.checkerframework:checker-qual:2.5.2 -- org.codehaus.mojo:animal-sniffer-annotations:1.17 +- org.checkerframework:checker-qual:3.33.0 This project bundles the following dependencies under BSD-2 License (https://opensource.org/licenses/BSD-2-Clause). See bundled license files for details. -- dnsjava:dnsjava:2.1.7 +- dnsjava:dnsjava:3.6.1 + +This project bundles the following dependencies under the Bouncy Castle License (https://www.bouncycastle.org/licence.html) + +- org.bouncycastle:bcprov-jdk18on:1.82 This project bundles the following dependencies under the Go License (https://golang.org/LICENSE). See bundled license files for details. @@ -52,3 +59,15 @@ This project bundles the following dependencies under BSD License (https://opens See bundled license files for details. - org.codehaus.woodstox:stax2-api:4.2.1 (https://github.com/FasterXML/stax2-api/tree/stax2-api-4.2.1) + +The bundled Apache Hadoop Relocated (Shaded) Third-party Miscellaneous Libs +org.apache.hadoop.thirdparty:hadoop-shaded-guava dependency bundles the following dependencies under +the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt) + +- com.google.guava:guava:33.4.8-jre + +The bundled Apache Hadoop Relocated (Shaded) Third-party Miscellaneous Libs +org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_25 dependency bundles the following dependencies under +the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt) + +- com.google.protobuf:protobuf-java:3.25.5 diff --git a/flink-filesystems/flink-oss-fs-hadoop/src/main/resources/META-INF/NOTICE b/flink-filesystems/flink-oss-fs-hadoop/src/main/resources/META-INF/NOTICE index 4c264e20ef01ff..03d720f5712fdd 100644 --- a/flink-filesystems/flink-oss-fs-hadoop/src/main/resources/META-INF/NOTICE +++ b/flink-filesystems/flink-oss-fs-hadoop/src/main/resources/META-INF/NOTICE @@ -16,7 +16,7 @@ This project bundles the following dependencies under the Apache Software Licens - io.opentracing:opentracing-api:0.33.0 - io.opentracing:opentracing-noop:0.33.0 - io.opentracing:opentracing-util:0.33.0 -- org.apache.hadoop:hadoop-aliyun:3.3.4 +- org.apache.hadoop:hadoop-aliyun:3.4.3 - org.apache.httpcomponents:httpclient:4.5.13 - org.apache.httpcomponents:httpcore:4.4.14 - org.codehaus.jettison:jettison:1.5.4 diff --git a/flink-filesystems/flink-s3-fs-base/pom.xml b/flink-filesystems/flink-s3-fs-base/pom.xml index fc3c6a66a54f3a..d146a498f176a8 100644 --- a/flink-filesystems/flink-s3-fs-base/pom.xml +++ b/flink-filesystems/flink-s3-fs-base/pom.xml @@ -32,6 +32,10 @@ under the License. 1.12.779 + + 2.44.4 true + + com.github.pjfanning + jersey-json + + + + com.sun.xml.bind + jaxb-impl + org.apache.avro avro @@ -234,6 +251,11 @@ under the License. com.amazonaws aws-java-sdk-bundle + + + software.amazon.awssdk + bundle + ch.qos.reload4j reload4j @@ -245,6 +267,38 @@ under the License. + + + software.amazon.awssdk + s3 + ${fs.s3.aws.sdk2.version} + + + software.amazon.awssdk + sts + ${fs.s3.aws.sdk2.version} + + + software.amazon.awssdk + kms + ${fs.s3.aws.sdk2.version} + + + software.amazon.awssdk + sso + ${fs.s3.aws.sdk2.version} + + + software.amazon.awssdk + auth + ${fs.s3.aws.sdk2.version} + + + software.amazon.awssdk + s3-transfer-manager + ${fs.s3.aws.sdk2.version} + + org.apache.flink diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/AbstractS3FileSystemFactory.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/AbstractS3FileSystemFactory.java index c96e71096949c1..7542366f14fce3 100644 --- a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/AbstractS3FileSystemFactory.java +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/AbstractS3FileSystemFactory.java @@ -186,7 +186,8 @@ public FileSystem create(URI fsUri) throws IOException { // create the Hadoop FileSystem org.apache.hadoop.conf.Configuration hadoopConfig = hadoopConfigLoader.getOrLoadHadoopConfig(); - AbstractS3DelegationTokenReceiver.updateHadoopConfig(hadoopConfig); + applyHadoopConfigDefaults(hadoopConfig); + updateDelegationTokenConfig(hadoopConfig); org.apache.hadoop.fs.FileSystem fs = createHadoopFileSystem(); fs.initialize(getInitURI(fsUri, hadoopConfig), hadoopConfig); @@ -258,4 +259,21 @@ protected FileSystem createFlinkFileSystem( @Nullable protected abstract S3AccessHelper getS3AccessHelper(org.apache.hadoop.fs.FileSystem fs); + + /** + * Applies Flink-side defaults to the loaded Hadoop configuration before the file system is + * initialized. Subclasses override this to pin Hadoop defaults that would otherwise change + * behavior across Hadoop upgrades. Implementations must only set keys the user has not + * configured and must stay idempotent, because the loaded Hadoop configuration is cached across + * {@link #create(URI)} calls. + */ + protected void applyHadoopConfigDefaults(org.apache.hadoop.conf.Configuration hadoopConfig) {} + + /** + * Updates the Hadoop configuration with delegation token credentials provider. Subclasses can + * override this method to use a different credentials provider (e.g., SDK v2 only). + */ + protected void updateDelegationTokenConfig(org.apache.hadoop.conf.Configuration hadoopConfig) { + AbstractS3DelegationTokenReceiver.updateHadoopConfig(hadoopConfig); + } } diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/FlinkS3FileSystem.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/FlinkS3FileSystem.java index f2a7cca5104903..3c0d243caa8b7e 100644 --- a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/FlinkS3FileSystem.java +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/FlinkS3FileSystem.java @@ -27,6 +27,7 @@ import org.apache.flink.core.fs.RefCountedFileWithStream; import org.apache.flink.core.fs.RefCountedTmpFileCreator; import org.apache.flink.fs.s3.common.token.AbstractS3DelegationTokenReceiver; +import org.apache.flink.fs.s3.common.token.S3SessionCredentials; import org.apache.flink.fs.s3.common.writer.S3AccessHelper; import org.apache.flink.fs.s3.common.writer.S3RecoverableWriter; import org.apache.flink.runtime.fs.hdfs.HadoopFileSystem; @@ -36,7 +37,6 @@ import org.apache.flink.util.StringUtils; import org.apache.flink.util.function.FunctionWithException; -import com.amazonaws.services.securitytoken.model.Credentials; import org.apache.hadoop.fs.FileSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -167,7 +167,7 @@ public static Optional of(Configuration flinkConfig) { } private void configureEnvironment(Map environment) { - Credentials credentials = AbstractS3DelegationTokenReceiver.getCredentials(); + S3SessionCredentials credentials = AbstractS3DelegationTokenReceiver.getCredentials(); if (credentials != null) { maybeSetEnvironmentVariable( environment, "AWS_ACCESS_KEY_ID", credentials.getAccessKeyId()); diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenProvider.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenProvider.java index 82e99dcde89568..fd1f02d29e2964 100644 --- a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenProvider.java +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenProvider.java @@ -25,18 +25,19 @@ import org.apache.flink.util.InstantiationUtil; import org.apache.flink.util.StringUtils; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.securitytoken.AWSSecurityTokenService; -import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; -import com.amazonaws.services.securitytoken.model.Credentials; -import com.amazonaws.services.securitytoken.model.GetSessionTokenResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Optional; -/** Delegation token provider for S3 filesystems. */ +/** + * Delegation token provider for S3 filesystems. + * + *

This class deliberately references no AWS SDK types (see {@link S3SessionCredentials}): the + * STS call to obtain session credentials is left to {@link #getSessionCredentials(String, String, + * String)}, implemented with AWS SDK v1 in {@code flink-s3-fs-presto} and with AWS SDK v2 in {@code + * flink-s3-fs-hadoop}. + */ @Internal public abstract class AbstractS3DelegationTokenProvider implements DelegationTokenProvider { @@ -51,24 +52,21 @@ public abstract class AbstractS3DelegationTokenProvider implements DelegationTok public void init(Configuration configuration) { region = configuration.getString(String.format("%s.region", serviceConfigPrefix()), null); if (!StringUtils.isNullOrWhitespaceOnly(region)) { - LOG.debug("Region: " + region); + LOG.debug("Region: {}", region); } accessKey = configuration.getString( String.format("%s.access-key", serviceConfigPrefix()), null); if (!StringUtils.isNullOrWhitespaceOnly(accessKey)) { - LOG.debug("Access key: " + accessKey); + LOG.debug("Access key: {}", accessKey); } secretKey = configuration.getString( String.format("%s.secret-key", serviceConfigPrefix()), null); if (!StringUtils.isNullOrWhitespaceOnly(secretKey)) { - LOG.debug( - "Secret key: " - + GlobalConfiguration.HIDDEN_CONTENT - + " (sensitive information)"); + LOG.debug("Secret key: {} (sensitive information)", GlobalConfiguration.HIDDEN_CONTENT); } } @@ -87,22 +85,21 @@ public boolean delegationTokensRequired() { public ObtainedDelegationTokens obtainDelegationTokens() throws Exception { LOG.info("Obtaining session credentials token with access key: {}", accessKey); - AWSSecurityTokenService stsClient = - AWSSecurityTokenServiceClientBuilder.standard() - .withRegion(region) - .withCredentials( - new AWSStaticCredentialsProvider( - new BasicAWSCredentials(accessKey, secretKey))) - .build(); - GetSessionTokenResult sessionTokenResult = stsClient.getSessionToken(); - Credentials credentials = sessionTokenResult.getCredentials(); + S3SessionCredentials credentials = getSessionCredentials(region, accessKey, secretKey); LOG.info( "Session credentials obtained successfully with access key: {} expiration: {}", credentials.getAccessKeyId(), - credentials.getExpiration()); + credentials.getExpirationEpochMilli()); return new ObtainedDelegationTokens( InstantiationUtil.serializeObject(credentials), - Optional.of(credentials.getExpiration().getTime())); + Optional.of(credentials.getExpirationEpochMilli())); } + + /** + * Obtains session credentials from AWS STS with the SDK bundled into the concrete filesystem + * plugin. + */ + protected abstract S3SessionCredentials getSessionCredentials( + String region, String accessKey, String secretKey) throws Exception; } diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenReceiver.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenReceiver.java index bad552f432b68e..420a1b5a452260 100644 --- a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenReceiver.java +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenReceiver.java @@ -26,13 +26,23 @@ import org.apache.flink.util.InstantiationUtil; import org.apache.flink.util.StringUtils; -import com.amazonaws.services.securitytoken.model.Credentials; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; -/** Delegation token receiver for S3 filesystems. */ +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Delegation token receiver for S3 filesystems. + * + *

This class deliberately references no AWS SDK types (see {@link S3SessionCredentials}), so it + * can be loaded both in the {@code flink-s3-fs-presto} plugin (AWS SDK v1 only) and the {@code + * flink-s3-fs-hadoop} plugin (AWS SDK v2 only). The received credentials are exposed through {@link + * #getCredentials()} to the SDK-specific credential providers and to SDK-agnostic consumers such as + * the s5cmd integration in {@code FlinkS3FileSystem}. + */ @Internal public abstract class AbstractS3DelegationTokenReceiver implements DelegationTokenReceiver { @@ -41,20 +51,45 @@ public abstract class AbstractS3DelegationTokenReceiver implements DelegationTok private static final Logger LOG = LoggerFactory.getLogger(AbstractS3DelegationTokenReceiver.class); - @VisibleForTesting @Nullable static volatile Credentials credentials; + @VisibleForTesting @Nullable static volatile S3SessionCredentials credentials; @VisibleForTesting @Nullable static volatile String region; public static void updateHadoopConfig(org.apache.hadoop.conf.Configuration hadoopConfig) { + updateHadoopConfig(hadoopConfig, DynamicTemporaryAWSCredentialsProvider.NAME); + } + + /** + * Registers the given delegation token credentials provider in {@code + * fs.s3a.aws.credentials.provider} and propagates the configured region. + * + *

When {@code credentialsProviderName} differs from the SDK v1 based {@link + * DynamicTemporaryAWSCredentialsProvider}, any user-configured reference to that legacy + * provider is remapped to {@code credentialsProviderName}: the legacy provider cannot be loaded + * in plugins that do not bundle AWS SDK v1 (e.g. {@code flink-s3-fs-hadoop}). + */ + public static void updateHadoopConfig( + org.apache.hadoop.conf.Configuration hadoopConfig, String credentialsProviderName) { LOG.info("Updating Hadoop configuration"); String providers = hadoopConfig.get(PROVIDER_CONFIG_NAME, ""); - if (!providers.contains(DynamicTemporaryAWSCredentialsProvider.NAME)) { + if (!credentialsProviderName.equals(DynamicTemporaryAWSCredentialsProvider.NAME)) { + String remappedProviders = replaceLegacyProvider(providers, credentialsProviderName); + if (!remappedProviders.equals(providers)) { + LOG.info( + "Remapped legacy SDK v1 credentials provider {} to {}", + DynamicTemporaryAWSCredentialsProvider.NAME, + credentialsProviderName); + providers = remappedProviders; + hadoopConfig.set(PROVIDER_CONFIG_NAME, providers); + } + } + if (!providers.contains(credentialsProviderName)) { if (providers.isEmpty()) { LOG.debug("Setting provider"); - providers = DynamicTemporaryAWSCredentialsProvider.NAME; + providers = credentialsProviderName; } else { - providers = DynamicTemporaryAWSCredentialsProvider.NAME + "," + providers; + providers = credentialsProviderName + "," + providers; LOG.debug("Prepending provider, new providers value: {}", providers); } hadoopConfig.set(PROVIDER_CONFIG_NAME, providers); @@ -70,6 +105,28 @@ public static void updateHadoopConfig(org.apache.hadoop.conf.Configuration hadoo LOG.info("Updated Hadoop configuration successfully"); } + /** + * Replaces the legacy SDK v1 delegation token credentials provider with its replacement, + * dropping duplicates while preserving the order of the remaining chain. Returns the input + * unchanged when the legacy provider is not referenced. + */ + private static String replaceLegacyProvider(String providers, String replacement) { + boolean legacyFound = false; + Set remapped = new LinkedHashSet<>(); + for (String provider : providers.split(",")) { + String trimmed = provider.trim(); + if (trimmed.isEmpty()) { + continue; + } + if (trimmed.equals(DynamicTemporaryAWSCredentialsProvider.NAME)) { + legacyFound = true; + trimmed = replacement; + } + remapped.add(trimmed); + } + return legacyFound ? String.join(",", remapped) : providers; + } + @Override public void init(Configuration configuration) { region = @@ -79,24 +136,25 @@ public void init(Configuration configuration) { DelegationTokenProvider.CONFIG_PREFIX, serviceName()), null); if (!StringUtils.isNullOrWhitespaceOnly(region)) { - LOG.debug("Region: " + region); + LOG.debug("Region: {}", region); } } @Override public void onNewTokensObtained(byte[] tokens) throws Exception { LOG.info("Updating session credentials"); - credentials = + S3SessionCredentials newCredentials = InstantiationUtil.deserializeObject( tokens, AbstractS3DelegationTokenReceiver.class.getClassLoader()); + credentials = newCredentials; LOG.info( "Session credentials updated successfully with access key: {} expiration: {}", - credentials.getAccessKeyId(), - credentials.getExpiration()); + newCredentials.getAccessKeyId(), + newCredentials.getExpirationEpochMilli()); } @Nullable - public static Credentials getCredentials() { + public static S3SessionCredentials getCredentials() { return credentials; } } diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/DynamicTemporaryAWSCredentialsProvider.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/DynamicTemporaryAWSCredentialsProvider.java index 36398448ca38ce..0d9d1522d6afb4 100644 --- a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/DynamicTemporaryAWSCredentialsProvider.java +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/DynamicTemporaryAWSCredentialsProvider.java @@ -21,12 +21,11 @@ import org.apache.flink.annotation.Internal; import com.amazonaws.SdkBaseException; +import com.amazonaws.SdkClientException; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.BasicSessionCredentials; -import com.amazonaws.services.securitytoken.model.Credentials; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.s3a.auth.NoAwsCredentialsException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,13 +34,26 @@ /** * Support dynamic session credentials for authenticating with AWS. Please note that users may * reference this class name from configuration property fs.s3a.aws.credentials.provider. Therefore, - * changing the class name would be a backward-incompatible change. This credential provider must - * not fail in creation because that will break a chain of credential providers. + * changing the class name would be a backward-incompatible change. This class is based on AWS SDK + * v1 and serves the flink-s3-fs-presto plugin; the SDK v2 based flink-s3-fs-hadoop plugin cannot + * load it and remaps this class name to {@code HadoopDynamicTemporaryAWSCredentialsProvider} in its + * Hadoop configuration. This credential provider must not fail in creation because that will break + * a chain of credential providers. When no credentials are available yet, {@link #getCredentials()} + * throws the SDK v1 {@link SdkClientException} (rather than Hadoop's s3a {@code + * NoAwsCredentialsException}, whose class hierarchy is based on AWS SDK v2 since Hadoop 3.4 and + * therefore cannot be loaded inside the flink-s3-fs-presto plugin, which bundles only SDK v1); + * credential provider chains treat any such exception as a signal to move on to the next provider. */ @Internal public class DynamicTemporaryAWSCredentialsProvider implements AWSCredentialsProvider { - public static final String NAME = DynamicTemporaryAWSCredentialsProvider.class.getName(); + /** + * Spelled out as a string literal (a compile-time constant) so that referencing {@code NAME} + * never triggers loading this class: it implements an SDK v1 interface that is absent from the + * flink-s3-fs-hadoop jar. Pinned to the actual class name by a test. + */ + public static final String NAME = + "org.apache.flink.fs.s3.common.token.DynamicTemporaryAWSCredentialsProvider"; public static final String COMPONENT = "Dynamic session credentials for Flink"; @@ -54,9 +66,9 @@ public DynamicTemporaryAWSCredentialsProvider(URI uri, Configuration conf) {} @Override public AWSCredentials getCredentials() throws SdkBaseException { - Credentials credentials = AbstractS3DelegationTokenReceiver.getCredentials(); + S3SessionCredentials credentials = AbstractS3DelegationTokenReceiver.getCredentials(); if (credentials == null) { - throw new NoAwsCredentialsException(COMPONENT); + throw new SdkClientException(COMPONENT + ": No AWS credentials"); } LOG.debug("Providing session credentials"); return new BasicSessionCredentials( @@ -67,6 +79,6 @@ public AWSCredentials getCredentials() throws SdkBaseException { @Override public void refresh() { - // Intentionally blank. Credentials are updated by S3DelegationTokenReceiver + // Intentionally blank. Credentials are updated by the delegation token receiver. } } diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/S3SessionCredentials.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/S3SessionCredentials.java new file mode 100644 index 00000000000000..ac5bcb77e6a3d3 --- /dev/null +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/token/S3SessionCredentials.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.fs.s3.common.token; + +import org.apache.flink.annotation.Internal; + +import javax.annotation.Nullable; + +import java.io.Serializable; + +/** + * AWS session credentials obtained through delegation tokens, held in an SDK-agnostic form. + * + *

This class deliberately references no AWS SDK types: it is bundled into both the {@code + * flink-s3-fs-hadoop} jar (which ships only AWS SDK v2) and the {@code flink-s3-fs-presto} jar + * (which ships only AWS SDK v1), so it must be loadable with either SDK absent. Each filesystem's + * credential provider converts these values into its own SDK's credential type. + * + *

Instances are Java-serialized as the delegation token payload sent from the JobManager-side + * {@link AbstractS3DelegationTokenProvider} to the TaskManager-side {@link + * AbstractS3DelegationTokenReceiver}. Both endpoints are always loaded from the same plugin jar, so + * the payload never crosses versions and is never persisted. + */ +@Internal +public final class S3SessionCredentials implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String accessKeyId; + + private final String secretAccessKey; + + @Nullable private final String sessionToken; + + private final long expirationEpochMilli; + + public S3SessionCredentials( + String accessKeyId, + String secretAccessKey, + @Nullable String sessionToken, + long expirationEpochMilli) { + this.accessKeyId = accessKeyId; + this.secretAccessKey = secretAccessKey; + this.sessionToken = sessionToken; + this.expirationEpochMilli = expirationEpochMilli; + } + + public String getAccessKeyId() { + return accessKeyId; + } + + public String getSecretAccessKey() { + return secretAccessKey; + } + + @Nullable + public String getSessionToken() { + return sessionToken; + } + + public long getExpirationEpochMilli() { + return expirationEpochMilli; + } +} diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/MultiPartUploadInfo.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/MultiPartUploadInfo.java index d76d2889512753..b4e9883be1339e 100644 --- a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/MultiPartUploadInfo.java +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/MultiPartUploadInfo.java @@ -20,7 +20,7 @@ import org.apache.flink.annotation.Internal; -import com.amazonaws.services.s3.model.PartETag; +import software.amazon.awssdk.services.s3.model.CompletedPart; import java.io.File; import java.util.ArrayList; @@ -38,7 +38,7 @@ final class MultiPartUploadInfo { private final String uploadId; - private final List completeParts; + private final List completeParts; private final Optional incompletePart; @@ -57,7 +57,7 @@ final class MultiPartUploadInfo { MultiPartUploadInfo( final String objectName, final String uploadId, - final List completeParts, + final List completeParts, final long numBytes, final Optional incompletePart) { @@ -92,7 +92,7 @@ Optional getIncompletePart() { return incompletePart; } - List getCopyOfEtagsOfCompleteParts() { + List getCopyOfEtagsOfCompleteParts() { return new ArrayList<>(completeParts); } @@ -101,7 +101,7 @@ void registerNewPart(long length) { this.numberOfRegisteredParts++; } - void registerCompletePart(PartETag eTag) { + void registerCompletePart(CompletedPart eTag) { completeParts.add(eTag); } diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/RecoverableMultiPartUploadImpl.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/RecoverableMultiPartUploadImpl.java index ddc6ceb83a6b56..4f53098419a939 100644 --- a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/RecoverableMultiPartUploadImpl.java +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/RecoverableMultiPartUploadImpl.java @@ -22,8 +22,8 @@ import org.apache.flink.annotation.VisibleForTesting; import org.apache.flink.core.fs.RefCountedFSOutputStream; -import com.amazonaws.services.s3.model.PartETag; -import com.amazonaws.services.s3.model.UploadPartResult; +import software.amazon.awssdk.services.s3.model.CompletedPart; +import software.amazon.awssdk.services.s3.model.UploadPartResponse; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; @@ -61,7 +61,7 @@ final class RecoverableMultiPartUploadImpl implements RecoverableMultiPartUpload private final Executor uploadThreadPool; - private final Deque> uploadsInProgress; + private final Deque> uploadsInProgress; private final String namePrefixForTempObjects; @@ -74,7 +74,7 @@ private RecoverableMultiPartUploadImpl( Executor uploadThreadPool, String uploadId, String objectName, - List partsSoFar, + List partsSoFar, long numBytes, Optional incompletePart) { checkArgument(numBytes >= 0L); @@ -102,7 +102,7 @@ public void uploadPart(RefCountedFSOutputStream file) throws IOException { // writing to the file we are uploading. checkState(file.isClosed()); - final CompletableFuture future = new CompletableFuture<>(); + final CompletableFuture future = new CompletableFuture<>(); uploadsInProgress.add(future); final long partLength = file.getPos(); @@ -153,7 +153,8 @@ public S3Recoverable snapshotAndGetRecoverable( final String objectName = currentUploadInfo.getObjectName(); final String uploadId = currentUploadInfo.getUploadId(); - final List completedParts = currentUploadInfo.getCopyOfEtagsOfCompleteParts(); + final List completedParts = + currentUploadInfo.getCopyOfEtagsOfCompleteParts(); final long sizeInBytes = currentUploadInfo.getExpectedSizeInBytes(); if (incompletePartObjectName == null) { @@ -193,6 +194,25 @@ private String safelyUploadSmallPart(@Nullable RefCountedFSOutputStream file) // utils // ------------------------------------------------------------------------ + /** + * Builds the {@link CompletedPart} for the CompleteMultipartUpload request from an upload-part + * response. Besides the ETag, every per-part checksum is copied: when checksum generation is + * enabled ({@code fs.s3a.checksum.generation}), S3 rejects a CompleteMultipartUpload whose + * parts do not repeat the checksum they were uploaded with. + */ + @VisibleForTesting + static CompletedPart completedPartFromResponse(int partNumber, UploadPartResponse response) { + final CompletedPart.Builder builder = + CompletedPart.builder().partNumber(partNumber).eTag(response.eTag()); + for (S3PartChecksum checksum : S3PartChecksum.values()) { + final String value = checksum.valueOf(response); + if (value != null) { + checksum.applyTo(builder, value); + } + } + return builder.build(); + } + @VisibleForTesting static String createIncompletePartObjectNamePrefix(String objectName) { checkNotNull(objectName); @@ -219,16 +239,16 @@ private void awaitPendingPartsUpload() throws IOException { checkState(currentUploadInfo.getRemainingParts() == uploadsInProgress.size()); while (currentUploadInfo.getRemainingParts() > 0) { - CompletableFuture next = uploadsInProgress.peekFirst(); - PartETag nextPart = awaitPendingPartUploadToComplete(next); + CompletableFuture next = uploadsInProgress.peekFirst(); + CompletedPart nextPart = awaitPendingPartUploadToComplete(next); currentUploadInfo.registerCompletePart(nextPart); uploadsInProgress.removeFirst(); } } - private PartETag awaitPendingPartUploadToComplete(CompletableFuture upload) + private CompletedPart awaitPendingPartUploadToComplete(CompletableFuture upload) throws IOException { - final PartETag completedUploadEtag; + final CompletedPart completedUploadEtag; try { completedUploadEtag = upload.get(); } catch (InterruptedException e) { @@ -267,7 +287,7 @@ public static RecoverableMultiPartUploadImpl recoverUpload( final Executor uploadThreadPool, final String multipartUploadId, final String objectName, - final List partsSoFar, + final List partsSoFar, final long numBytesSoFar, final Optional incompletePart) { @@ -297,13 +317,13 @@ private static class UploadTask implements Runnable { private final RefCountedFSOutputStream file; - private final CompletableFuture future; + private final CompletableFuture future; UploadTask( final S3AccessHelper s3AccessHelper, final MultiPartUploadInfo currentUpload, final RefCountedFSOutputStream file, - final CompletableFuture future) { + final CompletableFuture future) { checkNotNull(currentUpload); @@ -322,14 +342,14 @@ private static class UploadTask implements Runnable { @Override public void run() { try { - final UploadPartResult result = + final UploadPartResponse result = s3AccessHelper.uploadPart( objectName, uploadId, partNumber, file.getInputFile(), file.getPos()); - future.complete(new PartETag(result.getPartNumber(), result.getETag())); + future.complete(completedPartFromResponse(partNumber, result)); file.release(); } catch (Throwable t) { future.completeExceptionally(t); diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3AccessHelper.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3AccessHelper.java index 13f73ee4c9818d..31e1060a59c18f 100644 --- a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3AccessHelper.java +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3AccessHelper.java @@ -20,11 +20,11 @@ import org.apache.flink.annotation.Internal; -import com.amazonaws.services.s3.model.CompleteMultipartUploadResult; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PartETag; -import com.amazonaws.services.s3.model.PutObjectResult; -import com.amazonaws.services.s3.model.UploadPartResult; +import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse; +import software.amazon.awssdk.services.s3.model.CompletedPart; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.UploadPartResponse; import java.io.File; import java.io.IOException; @@ -33,12 +33,15 @@ /** * An interface that abstracts away the Multi-Part Upload (MPU) functionality offered by S3, from - * the specific implementation of the file system. This is needed so that we can accommodate both - * Hadoop S3 and Presto. + * the specific implementation of the file system. * - *

Multipart uploads are convenient for large object. These will be uploaded in multiple parts - * and the mutli-part upload is the equivalent of a transaction, where the upload with all its parts + *

Multipart uploads are convenient for large objects. These will be uploaded in multiple parts + * and the multipart upload is the equivalent of a transaction, where the upload with all its parts * will be either committed or discarded. + * + *

This interface uses AWS SDK v2 types and is implemented by the Hadoop S3A filesystem module. + * The Presto S3 filesystem (which uses AWS SDK v1) returns null for {@code S3AccessHelper} and + * handles uploads through its own mechanism. */ @Internal public interface S3AccessHelper { @@ -61,10 +64,10 @@ public interface S3AccessHelper { * @param partNumber the number of the part being uploaded (has to be in [1 ... 10000]). * @param inputFile the (local) file holding the part to be uploaded. * @param length the length of the part. - * @return The {@link UploadPartResult result} of the attempt to upload the part. + * @return The {@link UploadPartResponse result} of the attempt to upload the part. * @throws IOException */ - UploadPartResult uploadPart( + UploadPartResponse uploadPart( String key, String uploadId, int partNumber, File inputFile, long length) throws IOException; @@ -75,26 +78,27 @@ UploadPartResult uploadPart( * * @param key the key used to identify this part. * @param inputFile the (local) file holding the data to be uploaded. - * @return The {@link PutObjectResult result} of the attempt to stage the incomplete part. + * @return The {@link PutObjectResponse result} of the attempt to stage the incomplete part. * @throws IOException */ - PutObjectResult putObject(String key, File inputFile) throws IOException; + PutObjectResponse putObject(String key, File inputFile) throws IOException; /** * Finalizes a Multi-Part Upload. * * @param key the key identifying the object we finished uploading. * @param uploadId the id of the MPU. - * @param partETags the list of {@link PartETag ETags} associated with this MPU. + * @param partETags the list of {@link CompletedPart ETags} associated with this MPU. * @param length the size of the uploaded object. * @param errorCount a counter that will be used to count any failed attempts to commit the MPU. - * @return The {@link CompleteMultipartUploadResult result} of the attempt to finalize the MPU. + * @return The {@link CompleteMultipartUploadResponse result} of the attempt to finalize the + * MPU. * @throws IOException */ - CompleteMultipartUploadResult commitMultiPartUpload( + CompleteMultipartUploadResponse commitMultiPartUpload( String key, String uploadId, - List partETags, + List partETags, long length, AtomicInteger errorCount) throws IOException; @@ -124,8 +128,8 @@ CompleteMultipartUploadResult commitMultiPartUpload( * Fetches the metadata associated with a given key on S3. * * @param key the key. - * @return The associated {@link ObjectMetadata}. + * @return The associated {@link HeadObjectResponse}. * @throws IOException */ - ObjectMetadata getObjectMetadata(String key) throws IOException; + HeadObjectResponse getObjectMetadata(String key) throws IOException; } diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3Committer.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3Committer.java index 6cea633b30b05c..891b7644e70a59 100644 --- a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3Committer.java +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3Committer.java @@ -21,10 +21,10 @@ import org.apache.flink.core.fs.RecoverableFsDataOutputStream; import org.apache.flink.core.fs.RecoverableWriter; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PartETag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.s3.model.CompletedPart; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; import java.io.FileNotFoundException; import java.io.IOException; @@ -44,7 +44,7 @@ public final class S3Committer implements RecoverableFsDataOutputStream.Committe private final String objectName; - private final List parts; + private final List parts; private final long totalLength; @@ -52,7 +52,7 @@ public final class S3Committer implements RecoverableFsDataOutputStream.Committe S3AccessHelper s3AccessHelper, String objectName, String uploadId, - List parts, + List parts, long totalLength) { this.s3AccessHelper = checkNotNull(s3AccessHelper); this.objectName = checkNotNull(objectName); @@ -101,8 +101,8 @@ public void commitAfterRecovery() throws IOException { LOG.trace("Exception when committing:", e); try { - ObjectMetadata metadata = s3AccessHelper.getObjectMetadata(objectName); - if (totalLength != metadata.getContentLength()) { + HeadObjectResponse metadata = s3AccessHelper.getObjectMetadata(objectName); + if (totalLength != metadata.contentLength()) { String message = String.format( "Inconsistent result for object %s: conflicting lengths. " @@ -110,7 +110,7 @@ public void commitAfterRecovery() throws IOException { objectName, uploadId, totalLength, - metadata.getContentLength()); + metadata.contentLength()); LOG.warn(message); throw new IOException(message, e); } diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3PartChecksum.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3PartChecksum.java new file mode 100644 index 00000000000000..8d17db03f47988 --- /dev/null +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3PartChecksum.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.fs.s3.common.writer; + +import org.apache.flink.annotation.Internal; + +import software.amazon.awssdk.services.s3.model.CompletedPart; +import software.amazon.awssdk.services.s3.model.UploadPartResponse; + +import javax.annotation.Nullable; + +import java.io.IOException; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * The per-part checksums an S3 {@link UploadPartResponse} can carry, together with the stable wire + * tags used by {@link S3RecoverableSerializer}. + * + *

This enum is the single source of truth both for propagating checksums from {@link + * UploadPartResponse} to {@link CompletedPart} and for (de)serializing them in recoverable state, + * so the two sets can never drift apart. Adding a checksum field introduced by a newer AWS SDK only + * requires a new constant. Wire tags are persisted in checkpointed state and must never be + * renumbered or reused. + */ +@Internal +enum S3PartChecksum { + CRC32( + (byte) 1, + UploadPartResponse::checksumCRC32, + CompletedPart::checksumCRC32, + CompletedPart.Builder::checksumCRC32), + CRC32C( + (byte) 2, + UploadPartResponse::checksumCRC32C, + CompletedPart::checksumCRC32C, + CompletedPart.Builder::checksumCRC32C), + CRC64NVME( + (byte) 3, + UploadPartResponse::checksumCRC64NVME, + CompletedPart::checksumCRC64NVME, + CompletedPart.Builder::checksumCRC64NVME), + SHA1( + (byte) 4, + UploadPartResponse::checksumSHA1, + CompletedPart::checksumSHA1, + CompletedPart.Builder::checksumSHA1), + SHA256( + (byte) 5, + UploadPartResponse::checksumSHA256, + CompletedPart::checksumSHA256, + CompletedPart.Builder::checksumSHA256), + SHA512( + (byte) 6, + UploadPartResponse::checksumSHA512, + CompletedPart::checksumSHA512, + CompletedPart.Builder::checksumSHA512), + MD5( + (byte) 7, + UploadPartResponse::checksumMD5, + CompletedPart::checksumMD5, + CompletedPart.Builder::checksumMD5), + XXHASH64( + (byte) 8, + UploadPartResponse::checksumXXHASH64, + CompletedPart::checksumXXHASH64, + CompletedPart.Builder::checksumXXHASH64), + XXHASH3( + (byte) 9, + UploadPartResponse::checksumXXHASH3, + CompletedPart::checksumXXHASH3, + CompletedPart.Builder::checksumXXHASH3), + XXHASH128( + (byte) 10, + UploadPartResponse::checksumXXHASH128, + CompletedPart::checksumXXHASH128, + CompletedPart.Builder::checksumXXHASH128); + + private final byte wireTag; + + private final Function responseGetter; + + private final Function partGetter; + + private final BiConsumer builderSetter; + + S3PartChecksum( + byte wireTag, + Function responseGetter, + Function partGetter, + BiConsumer builderSetter) { + this.wireTag = wireTag; + this.responseGetter = responseGetter; + this.partGetter = partGetter; + this.builderSetter = builderSetter; + } + + byte getWireTag() { + return wireTag; + } + + @Nullable + String valueOf(UploadPartResponse response) { + return responseGetter.apply(response); + } + + @Nullable + String valueOf(CompletedPart part) { + return partGetter.apply(part); + } + + void applyTo(CompletedPart.Builder builder, String value) { + builderSetter.accept(builder, value); + } + + static S3PartChecksum fromWireTag(byte wireTag) throws IOException { + for (S3PartChecksum checksum : values()) { + if (checksum.wireTag == wireTag) { + return checksum; + } + } + throw new IOException("Corrupt data: Unknown part checksum tag " + wireTag); + } +} diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3Recoverable.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3Recoverable.java index cbe9e159b07fc6..d4757626529e85 100644 --- a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3Recoverable.java +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3Recoverable.java @@ -20,7 +20,7 @@ import org.apache.flink.core.fs.RecoverableWriter; -import com.amazonaws.services.s3.model.PartETag; +import software.amazon.awssdk.services.s3.model.CompletedPart; import javax.annotation.Nullable; @@ -36,7 +36,7 @@ public final class S3Recoverable implements RecoverableWriter.ResumeRecoverable private final String objectName; - private final List parts; + private final List parts; @Nullable private final String lastPartObject; @@ -44,14 +44,15 @@ public final class S3Recoverable implements RecoverableWriter.ResumeRecoverable private long lastPartObjectLength; - S3Recoverable(String objectName, String uploadId, List parts, long numBytesInParts) { + S3Recoverable( + String objectName, String uploadId, List parts, long numBytesInParts) { this(objectName, uploadId, parts, numBytesInParts, null, -1L); } S3Recoverable( String objectName, String uploadId, - List parts, + List parts, long numBytesInParts, @Nullable String lastPartObject, long lastPartObjectLength) { @@ -77,7 +78,7 @@ public String getObjectName() { return objectName; } - public List parts() { + public List parts() { return parts; } @@ -105,11 +106,11 @@ public String toString() { buf.append(", bytesInParts=").append(numBytesInParts); buf.append(", parts=["); int num = 0; - for (PartETag part : parts) { + for (CompletedPart part : parts) { if (0 != num++) { buf.append(", "); } - buf.append(part.getPartNumber()).append('=').append(part.getETag()); + buf.append(part.partNumber()).append('=').append(part.eTag()); } buf.append("], trailingPart=").append(lastPartObject); buf.append("trailingPartLen=").append(lastPartObjectLength); diff --git a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3RecoverableSerializer.java b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3RecoverableSerializer.java index a72e99be831d13..c5dfa13873abb0 100644 --- a/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3RecoverableSerializer.java +++ b/flink-filesystems/flink-s3-fs-base/src/main/java/org/apache/flink/fs/s3/common/writer/S3RecoverableSerializer.java @@ -21,7 +21,7 @@ import org.apache.flink.annotation.Internal; import org.apache.flink.core.io.SimpleVersionedSerializer; -import com.amazonaws.services.s3.model.PartETag; +import software.amazon.awssdk.services.s3.model.CompletedPart; import java.io.IOException; import java.nio.ByteBuffer; @@ -31,7 +31,14 @@ import java.util.ArrayList; import java.util.List; -/** Serializer implementation for a {@link S3Recoverable}. */ +/** + * Serializer implementation for a {@link S3Recoverable}. + * + *

Version 2 extends version 1 by persisting the per-part checksums (see {@link S3PartChecksum}) + * so that a CompleteMultipartUpload after recovery can repeat the checksum each part was uploaded + * with. Version-1 state deserializes into checksum-less parts, which is correct: it was written by + * releases that never generated part checksums. + */ @Internal final class S3RecoverableSerializer implements SimpleVersionedSerializer { @@ -46,22 +53,24 @@ private S3RecoverableSerializer() {} @Override public int getVersion() { - return 1; + return 2; } @Override public byte[] serialize(S3Recoverable obj) throws IOException { - final List partList = obj.parts(); - final PartETag[] parts = partList.toArray(new PartETag[partList.size()]); + final List partList = obj.parts(); + final CompletedPart[] parts = partList.toArray(new CompletedPart[0]); final byte[] keyBytes = obj.getObjectName().getBytes(CHARSET); final byte[] uploadIdBytes = obj.uploadId().getBytes(CHARSET); final byte[][] etags = new byte[parts.length][]; + final byte[][] checksumBlocks = new byte[parts.length][]; int partEtagBytes = 0; for (int i = 0; i < parts.length; i++) { - etags[i] = parts[i].getETag().getBytes(CHARSET); - partEtagBytes += etags[i].length + 2 * Integer.BYTES; + etags[i] = parts[i].eTag().getBytes(CHARSET); + checksumBlocks[i] = encodeChecksums(parts[i]); + partEtagBytes += etags[i].length + 2 * Integer.BYTES + checksumBlocks[i].length; } final String lastObjectKey = obj.incompleteObjectName(); @@ -93,10 +102,11 @@ public byte[] serialize(S3Recoverable obj) throws IOException { bb.putInt(etags.length); for (int i = 0; i < parts.length; i++) { - PartETag pe = parts[i]; - bb.putInt(pe.getPartNumber()); + CompletedPart pe = parts[i]; + bb.putInt(pe.partNumber()); bb.putInt(etags[i].length); bb.put(etags[i]); + bb.put(checksumBlocks[i]); } bb.putLong(obj.numBytesInParts()); @@ -113,11 +123,41 @@ public byte[] serialize(S3Recoverable obj) throws IOException { return targetBytes; } + /** + * Encodes the checksums of a part as: checksum count (byte, 0 for none), then per checksum its + * wire tag (byte) and the length-prefixed UTF-8 value exactly as the SDK returned it. + */ + private static byte[] encodeChecksums(CompletedPart part) { + final List presentChecksums = new ArrayList<>(); + final List values = new ArrayList<>(); + int size = Byte.BYTES; + for (S3PartChecksum checksum : S3PartChecksum.values()) { + final String value = checksum.valueOf(part); + if (value != null) { + final byte[] valueBytes = value.getBytes(CHARSET); + presentChecksums.add(checksum); + values.add(valueBytes); + size += Byte.BYTES + Integer.BYTES + valueBytes.length; + } + } + + final ByteBuffer bb = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + bb.put((byte) presentChecksums.size()); + for (int i = 0; i < presentChecksums.size(); i++) { + bb.put(presentChecksums.get(i).getWireTag()); + bb.putInt(values.get(i).length); + bb.put(values.get(i)); + } + return bb.array(); + } + @Override public S3Recoverable deserialize(int version, byte[] serialized) throws IOException { switch (version) { case 1: return deserializeV1(serialized); + case 2: + return deserializeV2(serialized); default: throw new IOException("Unrecognized version or corrupt state: " + version); } @@ -137,14 +177,58 @@ private static S3Recoverable deserializeV1(byte[] serialized) throws IOException bb.get(uploadIdBytes); final int numParts = bb.getInt(); - final ArrayList parts = new ArrayList<>(numParts); + final ArrayList parts = new ArrayList<>(numParts); + for (int i = 0; i < numParts; i++) { + final int partNum = bb.getInt(); + final byte[] buffer = new byte[bb.getInt()]; + bb.get(buffer); + parts.add( + CompletedPart.builder() + .partNumber(partNum) + .eTag(new String(buffer, CHARSET)) + .build()); + } + + return deserializeTrailer(bb, keyBytes, uploadIdBytes, parts); + } + + private static S3Recoverable deserializeV2(byte[] serialized) throws IOException { + final ByteBuffer bb = ByteBuffer.wrap(serialized).order(ByteOrder.LITTLE_ENDIAN); + + if (bb.getInt() != MAGIC_NUMBER) { + throw new IOException("Corrupt data: Unexpected magic number."); + } + + final byte[] keyBytes = new byte[bb.getInt()]; + bb.get(keyBytes); + + final byte[] uploadIdBytes = new byte[bb.getInt()]; + bb.get(uploadIdBytes); + + final int numParts = bb.getInt(); + final ArrayList parts = new ArrayList<>(numParts); for (int i = 0; i < numParts; i++) { final int partNum = bb.getInt(); final byte[] buffer = new byte[bb.getInt()]; bb.get(buffer); - parts.add(new PartETag(partNum, new String(buffer, CHARSET))); + final CompletedPart.Builder partBuilder = + CompletedPart.builder().partNumber(partNum).eTag(new String(buffer, CHARSET)); + final int numChecksums = bb.get(); + for (int c = 0; c < numChecksums; c++) { + final S3PartChecksum checksum = S3PartChecksum.fromWireTag(bb.get()); + final byte[] valueBuffer = new byte[bb.getInt()]; + bb.get(valueBuffer); + checksum.applyTo(partBuilder, new String(valueBuffer, CHARSET)); + } + parts.add(partBuilder.build()); } + return deserializeTrailer(bb, keyBytes, uploadIdBytes, parts); + } + + /** Reads the fields following the part list; identical in versions 1 and 2. */ + private static S3Recoverable deserializeTrailer( + ByteBuffer bb, byte[] keyBytes, byte[] uploadIdBytes, List parts) { final long numBytes = bb.getLong(); final String lastPart; diff --git a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/HAApplicationRunOnSeaweedFsS3StoreITCase.java b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/HAApplicationRunOnSeaweedFsS3StoreITCase.java index cb5a7521f430da..2827ee1b5d5f16 100644 --- a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/HAApplicationRunOnSeaweedFsS3StoreITCase.java +++ b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/HAApplicationRunOnSeaweedFsS3StoreITCase.java @@ -100,6 +100,12 @@ private static Configuration createConfiguration() { getSeaweedFsContainer().setS3ConfigOptions(config); + // S3A on Hadoop 3.4 keeps zero-byte directory markers by default + // (fs.s3a.directory.marker.retention=keep); pin the pre-3.4 "delete" policy so the raw + // listings below contain only actual result-store entries. The Presto subclass maps this + // key to presto.s3.directory.marker.retention, which PrestoS3FileSystem ignores. + config.setString("s3.directory.marker.retention", "delete"); + // ApplicationResultStore configuration config.set(ApplicationResultStoreOptions.DELETE_ON_COMMIT, Boolean.FALSE); config.set( diff --git a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/HAJobRunOnSeaweedFsS3StoreITCase.java b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/HAJobRunOnSeaweedFsS3StoreITCase.java index d047d36f2d0a6b..733daff0321fba 100644 --- a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/HAJobRunOnSeaweedFsS3StoreITCase.java +++ b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/HAJobRunOnSeaweedFsS3StoreITCase.java @@ -99,6 +99,12 @@ private static Configuration createConfiguration() { getSeaweedFsContainer().setS3ConfigOptions(config); + // S3A on Hadoop 3.4 keeps zero-byte directory markers by default + // (fs.s3a.directory.marker.retention=keep); pin the pre-3.4 "delete" policy so the raw + // listings below contain only actual result-store entries. The Presto subclass maps this + // key to presto.s3.directory.marker.retention, which PrestoS3FileSystem ignores. + config.setString("s3.directory.marker.retention", "delete"); + // JobResultStore configuration config.set(JobResultStoreOptions.DELETE_ON_COMMIT, Boolean.FALSE); config.set( diff --git a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenProviderTest.java b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenProviderTest.java index 43f3eb528f56fd..b5d231cc1b5dd3 100644 --- a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenProviderTest.java +++ b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenProviderTest.java @@ -19,10 +19,14 @@ package org.apache.flink.fs.s3.common.token; import org.apache.flink.configuration.Configuration; +import org.apache.flink.core.security.token.DelegationTokenProvider.ObtainedDelegationTokens; +import org.apache.flink.util.InstantiationUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Optional; + import static org.apache.flink.core.security.token.DelegationTokenProvider.CONFIG_PREFIX; import static org.assertj.core.api.Assertions.assertThat; @@ -32,18 +36,37 @@ class AbstractS3DelegationTokenProviderTest { private static final String REGION = "testRegion"; private static final String ACCESS_KEY_ID = "testAccessKeyId"; private static final String SECRET_ACCESS_KEY = "testSecretAccessKey"; + private static final String SESSION_TOKEN = "testSessionToken"; + private static final long EXPIRATION_EPOCH_MILLI = 1234567890L; + + /** Records the arguments of the STS call and returns fixed session credentials. */ + private static class TestS3DelegationTokenProvider extends AbstractS3DelegationTokenProvider { + + private String seenRegion; + private String seenAccessKey; + private String seenSecretKey; - private AbstractS3DelegationTokenProvider provider; + @Override + public String serviceName() { + return "s3"; + } + + @Override + protected S3SessionCredentials getSessionCredentials( + String region, String accessKey, String secretKey) { + this.seenRegion = region; + this.seenAccessKey = accessKey; + this.seenSecretKey = secretKey; + return new S3SessionCredentials( + ACCESS_KEY_ID, SECRET_ACCESS_KEY, SESSION_TOKEN, EXPIRATION_EPOCH_MILLI); + } + } + + private TestS3DelegationTokenProvider provider; @BeforeEach void beforeEach() { - provider = - new AbstractS3DelegationTokenProvider() { - @Override - public String serviceName() { - return "s3"; - } - }; + provider = new TestS3DelegationTokenProvider(); } @Test @@ -54,12 +77,36 @@ void delegationTokensRequiredShouldReturnFalseWithoutCredentials() { @Test void delegationTokensRequiredShouldReturnTrueWithCredentials() { + provider.init(createConfiguration()); + + assertThat(provider.delegationTokensRequired()).isTrue(); + } + + @Test + void obtainDelegationTokensShouldSerializeSessionCredentials() throws Exception { + provider.init(createConfiguration()); + + ObtainedDelegationTokens tokens = provider.obtainDelegationTokens(); + + assertThat(provider.seenRegion).isEqualTo(REGION); + assertThat(provider.seenAccessKey).isEqualTo(ACCESS_KEY_ID); + assertThat(provider.seenSecretKey).isEqualTo(SECRET_ACCESS_KEY); + assertThat(tokens.getValidUntil()).isEqualTo(Optional.of(EXPIRATION_EPOCH_MILLI)); + + S3SessionCredentials credentials = + InstantiationUtil.deserializeObject( + tokens.getTokens(), getClass().getClassLoader()); + assertThat(credentials.getAccessKeyId()).isEqualTo(ACCESS_KEY_ID); + assertThat(credentials.getSecretAccessKey()).isEqualTo(SECRET_ACCESS_KEY); + assertThat(credentials.getSessionToken()).isEqualTo(SESSION_TOKEN); + assertThat(credentials.getExpirationEpochMilli()).isEqualTo(EXPIRATION_EPOCH_MILLI); + } + + private static Configuration createConfiguration() { Configuration configuration = new Configuration(); configuration.setString(CONFIG_PREFIX + ".s3.region", REGION); configuration.setString(CONFIG_PREFIX + ".s3.access-key", ACCESS_KEY_ID); configuration.setString(CONFIG_PREFIX + ".s3.secret-key", SECRET_ACCESS_KEY); - provider.init(configuration); - - assertThat(provider.delegationTokensRequired()).isTrue(); + return configuration; } } diff --git a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenReceiverTest.java b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenReceiverTest.java index 5e579fda8cdeb1..e0b392ca5676b8 100644 --- a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenReceiverTest.java +++ b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/AbstractS3DelegationTokenReceiverTest.java @@ -19,6 +19,7 @@ package org.apache.flink.fs.s3.common.token; import org.apache.flink.configuration.Configuration; +import org.apache.flink.util.InstantiationUtil; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -32,16 +33,20 @@ class AbstractS3DelegationTokenReceiverTest { private static final String PROVIDER_CLASS_NAME = "TestProvider"; + private static final String SDK_V2_PROVIDER_CLASS_NAME = + "org.apache.flink.fs.s3.common.token.HadoopDynamicTemporaryAWSCredentialsProvider"; private static final String REGION = "testRegion"; @BeforeEach void beforeEach() { AbstractS3DelegationTokenReceiver.region = null; + AbstractS3DelegationTokenReceiver.credentials = null; } @AfterEach void afterEach() { AbstractS3DelegationTokenReceiver.region = null; + AbstractS3DelegationTokenReceiver.credentials = null; } @Test @@ -76,6 +81,46 @@ void updateHadoopConfigShouldNotAddProviderWhenAlreadyExists() { .isEqualTo(DynamicTemporaryAWSCredentialsProvider.NAME); } + @Test + void updateHadoopConfigShouldSetGivenProviderName() { + org.apache.hadoop.conf.Configuration hadoopConfiguration = + new org.apache.hadoop.conf.Configuration(); + hadoopConfiguration.set(PROVIDER_CONFIG_NAME, ""); + AbstractS3DelegationTokenReceiver.updateHadoopConfig( + hadoopConfiguration, SDK_V2_PROVIDER_CLASS_NAME); + assertThat(hadoopConfiguration.get(PROVIDER_CONFIG_NAME)) + .isEqualTo(SDK_V2_PROVIDER_CLASS_NAME); + } + + @Test + void updateHadoopConfigShouldRemapLegacyProviderName() { + // A user-configured reference to the SDK v1 provider must be remapped in plugins that + // register a different (SDK v2) provider, because the v1 provider class cannot be loaded + // there. + org.apache.hadoop.conf.Configuration hadoopConfiguration = + new org.apache.hadoop.conf.Configuration(); + hadoopConfiguration.set( + PROVIDER_CONFIG_NAME, + DynamicTemporaryAWSCredentialsProvider.NAME + "," + PROVIDER_CLASS_NAME); + AbstractS3DelegationTokenReceiver.updateHadoopConfig( + hadoopConfiguration, SDK_V2_PROVIDER_CLASS_NAME); + assertThat(hadoopConfiguration.get(PROVIDER_CONFIG_NAME)) + .isEqualTo(SDK_V2_PROVIDER_CLASS_NAME + "," + PROVIDER_CLASS_NAME); + } + + @Test + void updateHadoopConfigShouldDropDuplicateAfterRemapping() { + org.apache.hadoop.conf.Configuration hadoopConfiguration = + new org.apache.hadoop.conf.Configuration(); + hadoopConfiguration.set( + PROVIDER_CONFIG_NAME, + SDK_V2_PROVIDER_CLASS_NAME + "," + DynamicTemporaryAWSCredentialsProvider.NAME); + AbstractS3DelegationTokenReceiver.updateHadoopConfig( + hadoopConfiguration, SDK_V2_PROVIDER_CLASS_NAME); + assertThat(hadoopConfiguration.get(PROVIDER_CONFIG_NAME)) + .isEqualTo(SDK_V2_PROVIDER_CLASS_NAME); + } + @Test void updateHadoopConfigShouldNotUpdateRegionWhenNotConfigured() { AbstractS3DelegationTokenReceiver receiver = createReceiver(); @@ -100,6 +145,22 @@ void updateHadoopConfigShouldUpdateRegionWhenConfigured() { assertThat(hadoopConfiguration.get("fs.s3a.endpoint.region")).isEqualTo(REGION); } + @Test + void onNewTokensObtainedShouldStoreDeserializedCredentials() throws Exception { + AbstractS3DelegationTokenReceiver receiver = createReceiver(); + S3SessionCredentials credentials = + new S3SessionCredentials("accessKeyId", "secretAccessKey", "sessionToken", 42L); + + receiver.onNewTokensObtained(InstantiationUtil.serializeObject(credentials)); + + S3SessionCredentials storedCredentials = AbstractS3DelegationTokenReceiver.getCredentials(); + assertThat(storedCredentials).isNotNull(); + assertThat(storedCredentials.getAccessKeyId()).isEqualTo("accessKeyId"); + assertThat(storedCredentials.getSecretAccessKey()).isEqualTo("secretAccessKey"); + assertThat(storedCredentials.getSessionToken()).isEqualTo("sessionToken"); + assertThat(storedCredentials.getExpirationEpochMilli()).isEqualTo(42L); + } + private AbstractS3DelegationTokenReceiver createReceiver() { return new AbstractS3DelegationTokenReceiver() { @Override diff --git a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/DynamicTemporaryAWSCredentialsProviderTest.java b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/DynamicTemporaryAWSCredentialsProviderTest.java index 2a6c18a1024bf3..466b521f6e445b 100644 --- a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/DynamicTemporaryAWSCredentialsProviderTest.java +++ b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/DynamicTemporaryAWSCredentialsProviderTest.java @@ -20,9 +20,9 @@ import org.apache.flink.util.InstantiationUtil; +import com.amazonaws.SdkClientException; +import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.BasicSessionCredentials; -import com.amazonaws.services.securitytoken.model.Credentials; -import org.apache.hadoop.fs.s3a.auth.NoAwsCredentialsException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -36,6 +36,7 @@ class DynamicTemporaryAWSCredentialsProviderTest { private static final String ACCESS_KEY_ID = "testAccessKeyId"; private static final String SECRET_ACCESS_KEY = "testSecretAccessKey"; private static final String SESSION_TOKEN = "testSessionToken"; + private static final long EXPIRATION_EPOCH_MILLI = 1234567890L; @BeforeEach void beforeEach() { @@ -48,19 +49,32 @@ void afterEach() { } @Test - void getCredentialsShouldThrowExceptionWhenNoCredentials() { + void nameMustMatchClassName() { + // NAME is a string literal so that referencing it never class-loads this provider (it + // implements an SDK v1 interface absent from the flink-s3-fs-hadoop jar); this pins the + // literal to the actual class name, which users reference from + // fs.s3a.aws.credentials.provider. + assertThat(DynamicTemporaryAWSCredentialsProvider.NAME) + .isEqualTo(DynamicTemporaryAWSCredentialsProvider.class.getName()); + } + + @Test + void getCredentialsShouldThrowSdkV1ExceptionWhenNoCredentials() { DynamicTemporaryAWSCredentialsProvider provider = new DynamicTemporaryAWSCredentialsProvider(); - assertThatThrownBy(provider::getCredentials).isInstanceOf(NoAwsCredentialsException.class); + // Must be the SDK v1 exception: Hadoop's NoAwsCredentialsException is based on the AWS SDK + // v2 exception hierarchy since Hadoop 3.4 and cannot be loaded in the presto plugin. + assertThatThrownBy(provider::getCredentials).isInstanceOf(SdkClientException.class); } @Test - void getCredentialsShouldStoreCredentialsWhenCredentialsProvided() throws Exception { + void getCredentialsShouldReturnSessionCredentialsWhenProvided() throws Exception { DynamicTemporaryAWSCredentialsProvider provider = new DynamicTemporaryAWSCredentialsProvider(); - Credentials credentials = - new Credentials(ACCESS_KEY_ID, SECRET_ACCESS_KEY, SESSION_TOKEN, null); + S3SessionCredentials credentials = + new S3SessionCredentials( + ACCESS_KEY_ID, SECRET_ACCESS_KEY, SESSION_TOKEN, EXPIRATION_EPOCH_MILLI); AbstractS3DelegationTokenReceiver receiver = new AbstractS3DelegationTokenReceiver() { @Override @@ -70,11 +84,12 @@ public String serviceName() { }; receiver.onNewTokensObtained(InstantiationUtil.serializeObject(credentials)); - BasicSessionCredentials returnedCredentials = - (BasicSessionCredentials) provider.getCredentials(); - assertThat(returnedCredentials.getAWSAccessKeyId()).isEqualTo(credentials.getAccessKeyId()); - assertThat(returnedCredentials.getAWSSecretKey()) - .isEqualTo(credentials.getSecretAccessKey()); - assertThat(returnedCredentials.getSessionToken()).isEqualTo(credentials.getSessionToken()); + + AWSCredentials v1Credentials = provider.getCredentials(); + assertThat(v1Credentials).isInstanceOf(BasicSessionCredentials.class); + BasicSessionCredentials sessionCredentials = (BasicSessionCredentials) v1Credentials; + assertThat(sessionCredentials.getAWSAccessKeyId()).isEqualTo(ACCESS_KEY_ID); + assertThat(sessionCredentials.getAWSSecretKey()).isEqualTo(SECRET_ACCESS_KEY); + assertThat(sessionCredentials.getSessionToken()).isEqualTo(SESSION_TOKEN); } } diff --git a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/TokenSdkIsolationTest.java b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/TokenSdkIsolationTest.java new file mode 100644 index 00000000000000..68b0e8309c3091 --- /dev/null +++ b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/token/TokenSdkIsolationTest.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.fs.s3.common.token; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import org.junit.jupiter.api.Test; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +/** + * Guards the AWS SDK isolation of the shared classes in {@code flink-s3-fs-base}. + * + *

The classes bundled into both S3 filesystem plugins must not reference AWS SDK types: the + * {@code flink-s3-fs-presto} jar ships only AWS SDK v1 and the {@code flink-s3-fs-hadoop} jar only + * AWS SDK v2, so a stray dependency turns into {@code NoClassDefFoundError} at runtime in one of + * the plugins — invisible to unit tests, whose classpath contains both SDKs. + */ +class TokenSdkIsolationTest { + + private static final JavaClasses PRODUCTION_CLASSES = + new ClassFileImporter() + .withImportOption(new ImportOption.DoNotIncludeTests()) + .importPackages("org.apache.flink.fs.s3.common"); + + @Test + void tokenClassesMustStaySdkAgnostic() { + classes() + .that() + .resideInAPackage("org.apache.flink.fs.s3.common.token") + .and() + .doNotHaveFullyQualifiedName(DynamicTemporaryAWSCredentialsProvider.NAME) + .should() + .onlyDependOnClassesThat() + .resideOutsideOfPackages("software.amazon.awssdk..", "com.amazonaws..") + .because( + "token state is shared between the SDK v1 presto plugin and the SDK v2 " + + "hadoop plugin") + .check(PRODUCTION_CLASSES); + } + + @Test + void sdkV1CredentialsProviderMustNotDependOnSdkV2OrHadoopS3a() { + classes() + .that() + .haveFullyQualifiedName(DynamicTemporaryAWSCredentialsProvider.NAME) + .should() + .onlyDependOnClassesThat() + .resideOutsideOfPackages("software.amazon.awssdk..", "org.apache.hadoop.fs.s3a..") + .because( + "the provider is loaded inside the presto plugin where SDK v2 is absent, " + + "and Hadoop 3.4's s3a exception hierarchy is based on SDK v2") + .check(PRODUCTION_CLASSES); + } + + @Test + void flinkS3FileSystemMustStaySdkAgnostic() { + classes() + .that() + .haveNameMatching( + "org\\.apache\\.flink\\.fs\\.s3\\.common\\.FlinkS3FileSystem(\\$.*)?") + .should() + .onlyDependOnClassesThat() + .resideOutsideOfPackages("software.amazon.awssdk..", "com.amazonaws..") + .because( + "the s5cmd credential lookup runs in both plugins and must not touch " + + "SDK-specific credential types") + .check(PRODUCTION_CLASSES); + } +} diff --git a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/writer/RecoverableMultiPartUploadImplTest.java b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/writer/RecoverableMultiPartUploadImplTest.java index 7ec0101d2e95e5..654f5a1e808fd5 100644 --- a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/writer/RecoverableMultiPartUploadImplTest.java +++ b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/writer/RecoverableMultiPartUploadImplTest.java @@ -23,14 +23,14 @@ import org.apache.flink.util.IOUtils; import org.apache.flink.util.MathUtils; -import com.amazonaws.services.s3.model.CompleteMultipartUploadResult; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PartETag; -import com.amazonaws.services.s3.model.PutObjectResult; -import com.amazonaws.services.s3.model.UploadPartResult; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse; +import software.amazon.awssdk.services.s3.model.CompletedPart; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.UploadPartResponse; import java.io.File; import java.io.FileInputStream; @@ -141,6 +141,67 @@ void uploadingNonClosedFileAsCompleteShouldThroughException() throws IOException .isInstanceOf(IllegalStateException.class); } + @Test + void partChecksumsShouldBePropagatedToRecoverable() throws IOException { + stubMultiPartUploader.enableChecksumGeneration(); + final byte[] part = bytesOf("hello world"); + + uploadPart(part); + final S3Recoverable recoverable = multiPartUploadUnderTest.snapshotAndGetRecoverable(null); + + assertThat(recoverable.parts()).hasSize(1); + final CompletedPart completedPart = recoverable.parts().get(0); + assertThat(completedPart.partNumber()).isEqualTo(1); + assertThat(completedPart.checksumCRC32C()).isEqualTo(createCRC32CChecksum(1)); + } + + @Test + void completedPartFromResponseShouldCopyEveryChecksum() { + final UploadPartResponse response = + UploadPartResponse.builder() + .eTag("etag") + .checksumCRC32("crc32") + .checksumCRC32C("crc32c") + .checksumCRC64NVME("crc64nvme") + .checksumSHA1("sha1") + .checksumSHA256("sha256") + .checksumSHA512("sha512") + .checksumMD5("md5") + .checksumXXHASH64("xxhash64") + .checksumXXHASH3("xxhash3") + .checksumXXHASH128("xxhash128") + .build(); + + final CompletedPart part = + RecoverableMultiPartUploadImpl.completedPartFromResponse(7, response); + + assertThat(part.partNumber()).isEqualTo(7); + assertThat(part.eTag()).isEqualTo("etag"); + assertThat(part.checksumCRC32()).isEqualTo("crc32"); + assertThat(part.checksumCRC32C()).isEqualTo("crc32c"); + assertThat(part.checksumCRC64NVME()).isEqualTo("crc64nvme"); + assertThat(part.checksumSHA1()).isEqualTo("sha1"); + assertThat(part.checksumSHA256()).isEqualTo("sha256"); + assertThat(part.checksumSHA512()).isEqualTo("sha512"); + assertThat(part.checksumMD5()).isEqualTo("md5"); + assertThat(part.checksumXXHASH64()).isEqualTo("xxhash64"); + assertThat(part.checksumXXHASH3()).isEqualTo("xxhash3"); + assertThat(part.checksumXXHASH128()).isEqualTo("xxhash128"); + } + + @Test + void completedPartFromResponseShouldLeaveAbsentChecksumsUnset() { + final CompletedPart part = + RecoverableMultiPartUploadImpl.completedPartFromResponse( + 3, UploadPartResponse.builder().eTag("etag").build()); + + assertThat(part.partNumber()).isEqualTo(3); + assertThat(part.eTag()).isEqualTo("etag"); + for (S3PartChecksum checksum : S3PartChecksum.values()) { + assertThat(checksum.valueOf(part)).isNull(); + } + } + private static void assertThatHasMultiPartUploadWithPart( StubMultiPartUploader actual, byte[] content, int partNo) { TestUploadPartResult expectedCompletePart = @@ -168,8 +229,8 @@ private static void assertThatIsEqualTo( assertThat(actualRecoverable.incompleteObjectLength()) .isEqualTo(expectedRecoverable.incompleteObjectLength()); - assertThat(actualRecoverable.parts().stream().map(PartETag::getETag).toArray()) - .isEqualTo(expectedRecoverable.parts().stream().map(PartETag::getETag).toArray()); + assertThat(actualRecoverable.parts().stream().map(CompletedPart::eTag).toArray()) + .isEqualTo(expectedRecoverable.parts().stream().map(CompletedPart::eTag).toArray()); } // ---------------------------------- Test Methods ------------------------------------------- @@ -180,12 +241,16 @@ private static byte[] bytesOf(String str) { private static S3Recoverable createS3Recoverable( byte[] incompletePart, byte[]... completeParts) { - final List eTags = new ArrayList<>(); + final List parts = new ArrayList<>(); int index = 1; long bytesInPart = 0L; for (byte[] part : completeParts) { - eTags.add(new PartETag(index, createETag(TEST_OBJECT_NAME, index))); + parts.add( + CompletedPart.builder() + .partNumber(index) + .eTag(createETag(TEST_OBJECT_NAME, index)) + .build()); bytesInPart += part.length; index++; } @@ -193,7 +258,7 @@ private static S3Recoverable createS3Recoverable( return new S3Recoverable( TEST_OBJECT_NAME, createMPUploadId(TEST_OBJECT_NAME), - eTags, + parts, bytesInPart, "IGNORED-DUE-TO-RANDOMNESS", (long) incompletePart.length); @@ -201,21 +266,14 @@ private static S3Recoverable createS3Recoverable( private static RecoverableMultiPartUploadImplTest.TestPutObjectResult createPutObjectResult( String key, byte[] content) { - final RecoverableMultiPartUploadImplTest.TestPutObjectResult result = - new RecoverableMultiPartUploadImplTest.TestPutObjectResult(); - result.setETag(createETag(key, -1)); - result.setContent(content); - return result; + return new RecoverableMultiPartUploadImplTest.TestPutObjectResult( + createETag(key, -1), content); } private static RecoverableMultiPartUploadImplTest.TestUploadPartResult createUploadPartResult( String key, int number, byte[] payload) { - final RecoverableMultiPartUploadImplTest.TestUploadPartResult result = - new RecoverableMultiPartUploadImplTest.TestUploadPartResult(); - result.setETag(createETag(key, number)); - result.setPartNumber(number); - result.setContent(payload); - return result; + return new RecoverableMultiPartUploadImplTest.TestUploadPartResult( + number, createETag(key, number), payload); } private static String createMPUploadId(String key) { @@ -226,6 +284,10 @@ private static String createETag(String key, int partNo) { return "ETAG-" + key + '-' + partNo; } + private static String createCRC32CChecksum(int partNo) { + return "CRC32C-" + partNo; + } + private S3Recoverable uploadObject(byte[] content) throws IOException { final RefCountedBufferingFileStream incompletePartFile = writeContent(content); incompletePartFile.flush(); @@ -283,6 +345,16 @@ private static class StubMultiPartUploader implements S3AccessHelper { private final List incompletePartsUploaded = new ArrayList<>(); + private boolean checksumGenerationEnabled; + + /** + * Makes upload-part responses carry a per-part checksum, as S3 does when {@code + * fs.s3a.checksum.generation} is enabled. + */ + void enableChecksumGeneration() { + this.checksumGenerationEnabled = true; + } + List getCompletePartsUploaded() { return completePartsUploaded; } @@ -297,7 +369,7 @@ public String startMultiPartUpload(String key) throws IOException { } @Override - public UploadPartResult uploadPart( + public UploadPartResponse uploadPart( String key, String uploadId, int partNumber, File inputFile, long length) throws IOException { final byte[] content = @@ -306,7 +378,7 @@ public UploadPartResult uploadPart( } @Override - public PutObjectResult putObject(String key, File inputFile) throws IOException { + public PutObjectResponse putObject(String key, File inputFile) throws IOException { final byte[] content = getFileContentBytes(inputFile, MathUtils.checkedDownCast(inputFile.length())); return storeAndGetPutObjectResult(key, content); @@ -323,10 +395,10 @@ public long getObject(String key, File targetLocation) throws IOException { } @Override - public CompleteMultipartUploadResult commitMultiPartUpload( + public CompleteMultipartUploadResponse commitMultiPartUpload( String key, String uploadId, - List partETags, + List partETags, long length, AtomicInteger errorCount) throws IOException { @@ -334,7 +406,7 @@ public CompleteMultipartUploadResult commitMultiPartUpload( } @Override - public ObjectMetadata getObjectMetadata(String key) throws IOException { + public HeadObjectResponse getObjectMetadata(String key) throws IOException { throw new UnsupportedOperationException(); } @@ -344,37 +416,52 @@ private byte[] getFileContentBytes(File file, int length) throws IOException { return content; } - private RecoverableMultiPartUploadImplTest.TestUploadPartResult storeAndGetUploadPartResult( + private UploadPartResponse storeAndGetUploadPartResult( String key, int number, byte[] payload) { final RecoverableMultiPartUploadImplTest.TestUploadPartResult result = createUploadPartResult(key, number, payload); completePartsUploaded.add(result); - return result; + if (checksumGenerationEnabled) { + return result.toUploadPartResponse().toBuilder() + .checksumCRC32C(createCRC32CChecksum(number)) + .build(); + } + return result.toUploadPartResponse(); } - private RecoverableMultiPartUploadImplTest.TestPutObjectResult storeAndGetPutObjectResult( - String key, byte[] payload) { + private PutObjectResponse storeAndGetPutObjectResult(String key, byte[] payload) { final RecoverableMultiPartUploadImplTest.TestPutObjectResult result = createPutObjectResult(key, payload); incompletePartsUploaded.add(result); - return result; + return result.toPutObjectResponse(); } } - /** A {@link PutObjectResult} that also contains the actual content of the uploaded part. */ - private static class TestPutObjectResult extends PutObjectResult { - private static final long serialVersionUID = 1L; + /** + * A wrapper for {@link PutObjectResponse} that also contains the actual content of the uploaded + * part. + */ + private static class TestPutObjectResult { + private final String eTag; + private final byte[] content; - private byte[] content; + TestPutObjectResult(String eTag, byte[] content) { + this.eTag = eTag; + this.content = content; + } - void setContent(byte[] payload) { - this.content = payload; + public String getETag() { + return eTag; } public byte[] getContent() { return content; } + public PutObjectResponse toPutObjectResponse() { + return PutObjectResponse.builder().eTag(eTag).build(); + } + @Override public boolean equals(Object o) { if (this == o) { @@ -400,21 +487,37 @@ public String toString() { } } - /** A {@link UploadPartResult} that also contains the actual content of the uploaded part. */ - private static class TestUploadPartResult extends UploadPartResult { - - private static final long serialVersionUID = 1L; + /** + * A wrapper for {@link UploadPartResponse} that also contains the actual content of the + * uploaded part. + */ + private static class TestUploadPartResult { + private final int partNumber; + private final String eTag; + private final byte[] content; + + TestUploadPartResult(int partNumber, String eTag, byte[] content) { + this.partNumber = partNumber; + this.eTag = eTag; + this.content = content; + } - private byte[] content; + public String getETag() { + return eTag; + } - void setContent(byte[] content) { - this.content = content; + public int getPartNumber() { + return partNumber; } public byte[] getContent() { return content; } + public UploadPartResponse toUploadPartResponse() { + return UploadPartResponse.builder().eTag(eTag).build(); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/writer/S3RecoverableSerializerTest.java b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/writer/S3RecoverableSerializerTest.java index 58f7697f1bdb19..dc970a74b138c0 100644 --- a/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/writer/S3RecoverableSerializerTest.java +++ b/flink-filesystems/flink-s3-fs-base/src/test/java/org/apache/flink/fs/s3/common/writer/S3RecoverableSerializerTest.java @@ -18,14 +18,20 @@ package org.apache.flink.fs.s3.common.writer; -import com.amazonaws.services.s3.model.PartETag; import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.s3.model.CompletedPart; + +import javax.annotation.Nullable; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** Tests for the {@link S3RecoverableSerializer}. */ class S3RecoverableSerializerTest { @@ -40,12 +46,27 @@ class S3RecoverableSerializerTest { private static final String ETAG_PREFIX = "TEST-ETAG-"; + private static final String CRC32C_PREFIX = "TEST-CRC32C-"; + + private static final String SHA256_PREFIX = "TEST-SHA256-"; + + /** Version-2 wire tags of the checksums used by the fixtures; pinned independently. */ + private static final byte CRC32C_WIRE_TAG = 2; + + private static final byte SHA256_WIRE_TAG = 5; + + @Test + void serializerVersionIsTwo() { + assertThat(serializer.getVersion()).isEqualTo(2); + } + @Test void serializeEmptyS3Recoverable() throws IOException { S3Recoverable originalEmptyRecoverable = createTestS3Recoverable(false); byte[] serializedRecoverable = serializer.serialize(originalEmptyRecoverable); - S3Recoverable copiedEmptyRecoverable = serializer.deserialize(1, serializedRecoverable); + S3Recoverable copiedEmptyRecoverable = + serializer.deserialize(serializer.getVersion(), serializedRecoverable); assertThatIsEqualTo(originalEmptyRecoverable, copiedEmptyRecoverable); } @@ -56,7 +77,7 @@ void serializeS3RecoverableWithoutIncompleteObject() throws IOException { byte[] serializedRecoverable = serializer.serialize(originalNoIncompletePartRecoverable); S3Recoverable copiedNoIncompletePartRecoverable = - serializer.deserialize(1, serializedRecoverable); + serializer.deserialize(serializer.getVersion(), serializedRecoverable); assertThatIsEqualTo(originalNoIncompletePartRecoverable, copiedNoIncompletePartRecoverable); } @@ -67,7 +88,7 @@ void serializeS3RecoverableOnlyWithIncompleteObject() throws IOException { byte[] serializedRecoverable = serializer.serialize(originalOnlyIncompletePartRecoverable); S3Recoverable copiedOnlyIncompletePartRecoverable = - serializer.deserialize(1, serializedRecoverable); + serializer.deserialize(serializer.getVersion(), serializedRecoverable); assertThatIsEqualTo( originalOnlyIncompletePartRecoverable, copiedOnlyIncompletePartRecoverable); @@ -78,11 +99,109 @@ void serializeS3RecoverableWithCompleteAndIncompleteParts() throws IOException { S3Recoverable originalFullRecoverable = createTestS3Recoverable(true, 1, 5, 9); byte[] serializedRecoverable = serializer.serialize(originalFullRecoverable); - S3Recoverable copiedFullRecoverable = serializer.deserialize(1, serializedRecoverable); + S3Recoverable copiedFullRecoverable = + serializer.deserialize(serializer.getVersion(), serializedRecoverable); assertThatIsEqualTo(originalFullRecoverable, copiedFullRecoverable); } + @Test + void serializeS3RecoverableWithPartChecksums() throws IOException { + S3Recoverable originalChecksummedRecoverable = + createChecksummedTestS3Recoverable(true, 1, 5, 9); + + byte[] serializedRecoverable = serializer.serialize(originalChecksummedRecoverable); + S3Recoverable copiedChecksummedRecoverable = + serializer.deserialize(serializer.getVersion(), serializedRecoverable); + + assertThatIsEqualTo(originalChecksummedRecoverable, copiedChecksummedRecoverable); + } + + // ------------------------------------------------------------------------ + // Wire-format fixture tests. The serialized bytes are persisted in + // checkpoints and savepoints, so state written by previous releases must + // keep deserializing. These tests pin the exact byte layouts independently + // of the serializer implementation; if they fail, the layout changed and a + // new serializer version is required instead. + // ------------------------------------------------------------------------ + + @Test + void wireFormatV1StillDeserializes() throws IOException { + S3Recoverable expected = createTestS3Recoverable(true, 1, 5, 9); + byte[] v1Bytes = + buildV1WireBytes( + TEST_OBJECT_NAME, + TEST_UPLOAD_ID, + new int[] {1, 5, 9}, + 12345L, + INCOMPLETE_OBJECT_NAME, + 54321L); + + assertThatIsEqualTo(serializer.deserialize(1, v1Bytes), expected); + } + + @Test + void wireFormatV1StillDeserializesWithoutIncompleteObject() throws IOException { + S3Recoverable expected = createTestS3Recoverable(false, 1, 5, 9); + byte[] v1Bytes = + buildV1WireBytes( + TEST_OBJECT_NAME, TEST_UPLOAD_ID, new int[] {1, 5, 9}, 12345L, null, -1L); + + assertThatIsEqualTo(serializer.deserialize(1, v1Bytes), expected); + } + + @Test + void wireFormatV2IsStableWithoutChecksums() throws IOException { + S3Recoverable recoverable = createTestS3Recoverable(true, 1, 5, 9); + byte[] expectedBytes = + buildV2WireBytes( + TEST_OBJECT_NAME, + TEST_UPLOAD_ID, + new int[] {1, 5, 9}, + false, + 12345L, + INCOMPLETE_OBJECT_NAME, + 54321L); + + assertThat(serializer.serialize(recoverable)).isEqualTo(expectedBytes); + assertThatIsEqualTo(serializer.deserialize(2, expectedBytes), recoverable); + } + + @Test + void wireFormatV2IsStableWithChecksums() throws IOException { + S3Recoverable recoverable = createChecksummedTestS3Recoverable(false, 1, 5, 9); + byte[] expectedBytes = + buildV2WireBytes( + TEST_OBJECT_NAME, + TEST_UPLOAD_ID, + new int[] {1, 5, 9}, + true, + 12345L, + null, + -1L); + + assertThat(serializer.serialize(recoverable)).isEqualTo(expectedBytes); + assertThatIsEqualTo(serializer.deserialize(2, expectedBytes), recoverable); + } + + @Test + void unknownVersionIsRejected() throws IOException { + byte[] serialized = serializer.serialize(createTestS3Recoverable(false, 1)); + + assertThatThrownBy(() -> serializer.deserialize(3, serialized)) + .isInstanceOf(IOException.class) + .hasMessageContaining("Unrecognized version"); + } + + @Test + void unknownChecksumTagIsRejected() { + byte[] corruptBytes = buildV2WireBytesWithSingleChecksum((byte) 99, "SOME-VALUE"); + + assertThatThrownBy(() -> serializer.deserialize(2, corruptBytes)) + .isInstanceOf(IOException.class) + .hasMessageContaining("Unknown part checksum tag"); + } + private static void assertThatIsEqualTo( S3Recoverable actualRecoverable, S3Recoverable expectedRecoverable) { assertThat(actualRecoverable.getObjectName()) @@ -94,33 +213,220 @@ private static void assertThatIsEqualTo( .isEqualTo(expectedRecoverable.incompleteObjectName()); assertThat(actualRecoverable.incompleteObjectLength()) .isEqualTo(expectedRecoverable.incompleteObjectLength()); - assertThat(actualRecoverable.parts().stream().map(PartETag::getETag).toArray()) - .isEqualTo(expectedRecoverable.parts().stream().map(PartETag::getETag).toArray()); + // full CompletedPart equality, covering the checksum fields + assertThat(actualRecoverable.parts().toArray()) + .isEqualTo(expectedRecoverable.parts().toArray()); } // --------------------------------- Test Utils --------------------------------- private static S3Recoverable createTestS3Recoverable( boolean withIncompletePart, int... partNumbers) { - List etags = new ArrayList<>(); + return createTestS3Recoverable(false, withIncompletePart, partNumbers); + } + + private static S3Recoverable createChecksummedTestS3Recoverable( + boolean withIncompletePart, int... partNumbers) { + return createTestS3Recoverable(true, withIncompletePart, partNumbers); + } + + private static S3Recoverable createTestS3Recoverable( + boolean withChecksums, boolean withIncompletePart, int... partNumbers) { + List parts = new ArrayList<>(); for (int i : partNumbers) { - etags.add(createEtag(i)); + parts.add(createCompletedPart(i, withChecksums)); } if (withIncompletePart) { return new S3Recoverable( TEST_OBJECT_NAME, TEST_UPLOAD_ID, - etags, + parts, 12345L, INCOMPLETE_OBJECT_NAME, 54321L); } else { - return new S3Recoverable(TEST_OBJECT_NAME, TEST_UPLOAD_ID, etags, 12345L); + return new S3Recoverable(TEST_OBJECT_NAME, TEST_UPLOAD_ID, parts, 12345L); + } + } + + private static CompletedPart createCompletedPart(int partNumber, boolean withChecksums) { + CompletedPart.Builder builder = + CompletedPart.builder().partNumber(partNumber).eTag(ETAG_PREFIX + partNumber); + if (withChecksums) { + builder.checksumCRC32C(CRC32C_PREFIX + partNumber) + .checksumSHA256(SHA256_PREFIX + partNumber); + } + return builder.build(); + } + + /** + * Hand-builds the version-1 wire layout: little-endian; magic number (int); key length (int) + + * UTF-8 bytes; upload id length (int) + bytes; part count (int); per part: part number (int) + + * etag length (int) + bytes; bytes in parts (long); incomplete object name length (int, 0 for + * none) + bytes; incomplete object length (long). + */ + private static byte[] buildV1WireBytes( + String objectName, + String uploadId, + int[] partNumbers, + long numBytesInParts, + @Nullable String incompleteObjectName, + long incompleteObjectLength) { + final byte[] keyBytes = objectName.getBytes(StandardCharsets.UTF_8); + final byte[] uploadIdBytes = uploadId.getBytes(StandardCharsets.UTF_8); + final byte[][] etags = new byte[partNumbers.length][]; + int partBytes = 0; + for (int i = 0; i < partNumbers.length; i++) { + etags[i] = (ETAG_PREFIX + partNumbers[i]).getBytes(StandardCharsets.UTF_8); + partBytes += 2 * Integer.BYTES + etags[i].length; + } + final byte[] incompleteBytes = + incompleteObjectName == null + ? new byte[0] + : incompleteObjectName.getBytes(StandardCharsets.UTF_8); + + final ByteBuffer bb = + ByteBuffer.allocate( + 4 * Integer.BYTES + + keyBytes.length + + uploadIdBytes.length + + partBytes + + 2 * Long.BYTES + + Integer.BYTES + + incompleteBytes.length) + .order(ByteOrder.LITTLE_ENDIAN); + + bb.putInt(0x98761432); + bb.putInt(keyBytes.length); + bb.put(keyBytes); + bb.putInt(uploadIdBytes.length); + bb.put(uploadIdBytes); + bb.putInt(partNumbers.length); + for (int i = 0; i < partNumbers.length; i++) { + bb.putInt(partNumbers[i]); + bb.putInt(etags[i].length); + bb.put(etags[i]); + } + bb.putLong(numBytesInParts); + bb.putInt(incompleteBytes.length); + bb.put(incompleteBytes); + bb.putLong(incompleteObjectLength); + + return bb.array(); + } + + /** + * Hand-builds the version-2 wire layout: like version 1, but each part is followed by its + * checksum count (byte) and per checksum the wire tag (byte) + value length (int) + UTF-8 + * bytes. Parts built with {@code withChecksums} carry a CRC32C (tag 2) and a SHA256 (tag 5) + * checksum, in wire-tag order. + */ + private static byte[] buildV2WireBytes( + String objectName, + String uploadId, + int[] partNumbers, + boolean withChecksums, + long numBytesInParts, + @Nullable String incompleteObjectName, + long incompleteObjectLength) { + final byte[] keyBytes = objectName.getBytes(StandardCharsets.UTF_8); + final byte[] uploadIdBytes = uploadId.getBytes(StandardCharsets.UTF_8); + final byte[][] etags = new byte[partNumbers.length][]; + final byte[][] crc32cs = new byte[partNumbers.length][]; + final byte[][] sha256s = new byte[partNumbers.length][]; + int partBytes = 0; + for (int i = 0; i < partNumbers.length; i++) { + etags[i] = (ETAG_PREFIX + partNumbers[i]).getBytes(StandardCharsets.UTF_8); + partBytes += 2 * Integer.BYTES + etags[i].length + Byte.BYTES; + if (withChecksums) { + crc32cs[i] = (CRC32C_PREFIX + partNumbers[i]).getBytes(StandardCharsets.UTF_8); + sha256s[i] = (SHA256_PREFIX + partNumbers[i]).getBytes(StandardCharsets.UTF_8); + partBytes += + 2 * (Byte.BYTES + Integer.BYTES) + crc32cs[i].length + sha256s[i].length; + } + } + final byte[] incompleteBytes = + incompleteObjectName == null + ? new byte[0] + : incompleteObjectName.getBytes(StandardCharsets.UTF_8); + + final ByteBuffer bb = + ByteBuffer.allocate( + 4 * Integer.BYTES + + keyBytes.length + + uploadIdBytes.length + + partBytes + + 2 * Long.BYTES + + Integer.BYTES + + incompleteBytes.length) + .order(ByteOrder.LITTLE_ENDIAN); + + bb.putInt(0x98761432); + bb.putInt(keyBytes.length); + bb.put(keyBytes); + bb.putInt(uploadIdBytes.length); + bb.put(uploadIdBytes); + bb.putInt(partNumbers.length); + for (int i = 0; i < partNumbers.length; i++) { + bb.putInt(partNumbers[i]); + bb.putInt(etags[i].length); + bb.put(etags[i]); + if (withChecksums) { + bb.put((byte) 2); + bb.put(CRC32C_WIRE_TAG); + bb.putInt(crc32cs[i].length); + bb.put(crc32cs[i]); + bb.put(SHA256_WIRE_TAG); + bb.putInt(sha256s[i].length); + bb.put(sha256s[i]); + } else { + bb.put((byte) 0); + } } + bb.putLong(numBytesInParts); + bb.putInt(incompleteBytes.length); + bb.put(incompleteBytes); + bb.putLong(incompleteObjectLength); + + return bb.array(); } - private static PartETag createEtag(int partNumber) { - return new PartETag(partNumber, ETAG_PREFIX + partNumber); + /** Builds version-2 bytes for a single one-part recoverable carrying one checksum entry. */ + private static byte[] buildV2WireBytesWithSingleChecksum(byte wireTag, String checksumValue) { + final byte[] keyBytes = TEST_OBJECT_NAME.getBytes(StandardCharsets.UTF_8); + final byte[] uploadIdBytes = TEST_UPLOAD_ID.getBytes(StandardCharsets.UTF_8); + final byte[] etagBytes = (ETAG_PREFIX + 1).getBytes(StandardCharsets.UTF_8); + final byte[] checksumBytes = checksumValue.getBytes(StandardCharsets.UTF_8); + + final ByteBuffer bb = + ByteBuffer.allocate( + 8 * Integer.BYTES + + keyBytes.length + + uploadIdBytes.length + + etagBytes.length + + 2 * Byte.BYTES + + checksumBytes.length + + 2 * Long.BYTES) + .order(ByteOrder.LITTLE_ENDIAN); + + bb.putInt(0x98761432); + bb.putInt(keyBytes.length); + bb.put(keyBytes); + bb.putInt(uploadIdBytes.length); + bb.put(uploadIdBytes); + bb.putInt(1); + bb.putInt(1); + bb.putInt(etagBytes.length); + bb.put(etagBytes); + bb.put((byte) 1); + bb.put(wireTag); + bb.putInt(checksumBytes.length); + bb.put(checksumBytes); + bb.putLong(12345L); + bb.putInt(0); + bb.putLong(-1L); + + return bb.array(); } } diff --git a/flink-filesystems/flink-s3-fs-hadoop/pom.xml b/flink-filesystems/flink-s3-fs-hadoop/pom.xml index 098296e49561a7..98a6eea8cd5565 100644 --- a/flink-filesystems/flink-s3-fs-hadoop/pom.xml +++ b/flink-filesystems/flink-s3-fs-hadoop/pom.xml @@ -84,12 +84,43 @@ under the License. test - + org.apache.flink flink-s3-fs-base ${project.version} ${flink.markBundledAsOptional} + + + com.amazonaws + aws-java-sdk-core + + + com.amazonaws + aws-java-sdk-s3 + + + com.amazonaws + aws-java-sdk-kms + + + com.amazonaws + aws-java-sdk-dynamodb + + + com.amazonaws + aws-java-sdk-sts + + @@ -284,14 +315,27 @@ under the License. - com.amazonaws:aws-java-sdk-s3 - + org.apache.flink:flink-s3-fs-base + com/amazonaws/services/s3/model/transform/XmlResponsesSaxParser** + + + software.amazon.s3.analyticsaccelerator:analyticsaccelerator-s3 + + LICENSE + NOTICE + THIRD-PARTY-NOTICES + + diff --git a/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3/common/token/HadoopDynamicTemporaryAWSCredentialsProvider.java b/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3/common/token/HadoopDynamicTemporaryAWSCredentialsProvider.java new file mode 100644 index 00000000000000..80b00ee88da784 --- /dev/null +++ b/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3/common/token/HadoopDynamicTemporaryAWSCredentialsProvider.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.fs.s3.common.token; + +import org.apache.flink.annotation.Internal; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.s3a.auth.NoAwsCredentialsException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.core.exception.SdkException; + +import java.net.URI; + +/** + * Support dynamic session credentials for authenticating with AWS using SDK v2. Please note that + * users may reference this class name from configuration property fs.s3a.aws.credentials.provider. + * Therefore, changing the class name would be a backward-incompatible change. + * + *

This credential provider must not fail during construction, because that would break the chain + * of credential providers it is registered in. It is, however, expected to fail fast from {@link + * #resolveCredentials()} by throwing {@link NoAwsCredentialsException} when no delegation token + * credentials are available yet; the surrounding {@code AwsCredentialsProviderChain} treats that as + * a signal to move on to the next provider in the chain. + */ +@Internal +public class HadoopDynamicTemporaryAWSCredentialsProvider implements AwsCredentialsProvider { + + /** + * Spelled out as a string literal (a compile-time constant) so that referencing {@code NAME} + * never triggers loading this class outside the flink-s3-fs-hadoop plugin. Pinned to the actual + * class name by a test. + */ + public static final String NAME = + "org.apache.flink.fs.s3.common.token.HadoopDynamicTemporaryAWSCredentialsProvider"; + + public static final String COMPONENT = "Dynamic session credentials for Flink (SDK v2)"; + + private static final Logger LOG = + LoggerFactory.getLogger(HadoopDynamicTemporaryAWSCredentialsProvider.class); + + public HadoopDynamicTemporaryAWSCredentialsProvider() {} + + public HadoopDynamicTemporaryAWSCredentialsProvider(URI uri, Configuration conf) {} + + @Override + public AwsCredentials resolveCredentials() throws SdkException { + S3SessionCredentials credentials = AbstractS3DelegationTokenReceiver.getCredentials(); + if (credentials == null) { + throw new NoAwsCredentialsException(COMPONENT); + } + LOG.debug("Providing session credentials"); + return AwsSessionCredentials.create( + credentials.getAccessKeyId(), + credentials.getSecretAccessKey(), + credentials.getSessionToken()); + } +} diff --git a/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/HadoopS3AccessHelper.java b/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/HadoopS3AccessHelper.java index c93dfb6de49859..5c21805e875147 100644 --- a/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/HadoopS3AccessHelper.java +++ b/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/HadoopS3AccessHelper.java @@ -19,24 +19,22 @@ package org.apache.flink.fs.s3hadoop; import org.apache.flink.fs.s3.common.writer.S3AccessHelper; -import org.apache.flink.util.MathUtils; - -import com.amazonaws.SdkBaseException; -import com.amazonaws.services.s3.model.CompleteMultipartUploadResult; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PartETag; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.amazonaws.services.s3.model.PutObjectResult; -import com.amazonaws.services.s3.model.UploadPartRequest; -import com.amazonaws.services.s3.model.UploadPartResult; + import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.s3a.S3ADataBlocks; import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.S3AUtils; import org.apache.hadoop.fs.s3a.WriteOperationHelper; -import org.apache.hadoop.fs.s3a.statistics.S3AStatisticsContext; -import org.apache.hadoop.fs.store.audit.AuditSpan; -import org.apache.hadoop.fs.store.audit.AuditSpanSource; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse; +import software.amazon.awssdk.services.s3.model.CompletedPart; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.UploadPartRequest; +import software.amazon.awssdk.services.s3.model.UploadPartResponse; import java.io.File; import java.io.FileOutputStream; @@ -47,62 +45,67 @@ import static org.apache.flink.util.Preconditions.checkNotNull; -/** An implementation of the {@link S3AccessHelper} for the Hadoop S3A filesystem. */ +/** + * An implementation of the {@link S3AccessHelper} for the Hadoop S3A filesystem. + * + *

This implementation uses Hadoop's {@link WriteOperationHelper} to perform S3 operations, + * similar to the SDK v1 implementation. This provides retry logic, error handling, S3A statistics + * integration, and auditing support. + */ public class HadoopS3AccessHelper implements S3AccessHelper { private final S3AFileSystem s3a; - private final InternalWriteOperationHelper s3accessHelper; + private final WriteOperationHelper writeHelper; + + private final PutObjectOptions putOptions; public HadoopS3AccessHelper(S3AFileSystem s3a, Configuration conf) { - checkNotNull(s3a); - this.s3accessHelper = - new InternalWriteOperationHelper( - s3a, - checkNotNull(conf), - s3a.createStoreContext().getInstrumentation(), - s3a.getAuditSpanSource(), - s3a.getActiveAuditSpan()); - this.s3a = s3a; + this.s3a = checkNotNull(s3a); + // Get WriteOperationHelper from S3AFileSystem which properly initializes + // it with callbacks, statistics, and audit support + this.writeHelper = s3a.getWriteOperationHelper(); + this.putOptions = PutObjectOptions.defaultOptions(); } @Override public String startMultiPartUpload(String key) throws IOException { - return s3accessHelper.initiateMultiPartUpload(key); + return writeHelper.initiateMultiPartUpload(key, putOptions); } @Override - public UploadPartResult uploadPart( + public UploadPartResponse uploadPart( String key, String uploadId, int partNumber, File inputFile, long length) throws IOException { - final UploadPartRequest uploadRequest = - s3accessHelper.newUploadPartRequest( - key, - uploadId, - partNumber, - MathUtils.checkedDownCast(length), - null, - inputFile, - 0L); - return s3accessHelper.uploadPart(uploadRequest); + UploadPartRequest request = + writeHelper + .newUploadPartRequestBuilder(key, uploadId, partNumber, false, length) + .build(); + RequestBody body = RequestBody.fromFile(inputFile); + return writeHelper.uploadPart(request, body, null); } @Override - public PutObjectResult putObject(String key, File inputFile) throws IOException { - final PutObjectRequest putRequest = s3accessHelper.createPutObjectRequest(key, inputFile); - return s3accessHelper.putObject(putRequest); + public PutObjectResponse putObject(String key, File inputFile) throws IOException { + // Use WriteOperationHelper for retry logic, statistics, and audit tracking + software.amazon.awssdk.services.s3.model.PutObjectRequest request = + writeHelper.createPutObjectRequest(key, inputFile.length(), putOptions); + try (S3ADataBlocks.BlockUploadData uploadData = + new S3ADataBlocks.BlockUploadData(inputFile, () -> true)) { + return writeHelper.putObject(request, putOptions, uploadData, null); + } } @Override - public CompleteMultipartUploadResult commitMultiPartUpload( + public CompleteMultipartUploadResponse commitMultiPartUpload( String destKey, String uploadId, - List partETags, + List partETags, long length, AtomicInteger errorCount) throws IOException { - return s3accessHelper.completeMPUwithRetries( - destKey, uploadId, partETags, length, errorCount); + return writeHelper.completeMPUwithRetries( + destKey, uploadId, partETags, length, errorCount, putOptions); } @Override @@ -139,27 +142,11 @@ public long getObject(String key, File targetLocation) throws IOException { } @Override - public ObjectMetadata getObjectMetadata(String key) throws IOException { + public HeadObjectResponse getObjectMetadata(String key) throws IOException { try { return s3a.getObjectMetadata(new Path('/' + key)); - } catch (SdkBaseException e) { + } catch (SdkException e) { throw S3AUtils.translateException("getObjectMetadata", key, e); } } - - /** - * Internal {@link WriteOperationHelper} that is wrapped so that it only exposes the - * functionality we need for the {@link S3AccessHelper}. - */ - private static final class InternalWriteOperationHelper extends WriteOperationHelper { - - InternalWriteOperationHelper( - S3AFileSystem owner, - Configuration conf, - S3AStatisticsContext statisticsContext, - AuditSpanSource auditSpanSource, - AuditSpan auditSpan) { - super(owner, conf, statisticsContext, auditSpanSource, auditSpan); - } - } } diff --git a/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/S3FileSystemFactory.java b/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/S3FileSystemFactory.java index 37f984f630a90c..cead2f60f370fe 100644 --- a/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/S3FileSystemFactory.java +++ b/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/S3FileSystemFactory.java @@ -20,10 +20,13 @@ import org.apache.flink.annotation.VisibleForTesting; import org.apache.flink.fs.s3.common.AbstractS3FileSystemFactory; +import org.apache.flink.fs.s3.common.token.AbstractS3DelegationTokenReceiver; +import org.apache.flink.fs.s3.common.token.HadoopDynamicTemporaryAWSCredentialsProvider; import org.apache.flink.fs.s3.common.writer.S3AccessHelper; import org.apache.flink.runtime.util.HadoopConfigLoader; import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.s3a.Constants; import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -105,4 +108,25 @@ protected S3AccessHelper getS3AccessHelper(FileSystem fs) { final S3AFileSystem s3Afs = (S3AFileSystem) fs; return new HadoopS3AccessHelper(s3Afs, s3Afs.getConf()); } + + @Override + protected void applyHadoopConfigDefaults(org.apache.hadoop.conf.Configuration hadoopConfig) { + // hadoop-aws 3.4.3 defaults fs.s3a.input.stream.type to the S3 Analytics Accelerator + // stream (up to 3.4.2 the default was the classic stream), which changes the read + // pattern (and S3 GET volume) of every s3a:// read. Default to the classic stream so + // the Hadoop upgrade is behavior-preserving; users opt in via + // s3.input.stream.type=analytics. The legacy prefetch toggle takes precedence in Hadoop + // when the stream type is unset, so it must not be overridden. + if (hadoopConfig.get(Constants.INPUT_STREAM_TYPE) == null + && !hadoopConfig.getBoolean( + Constants.PREFETCH_ENABLED_KEY, Constants.PREFETCH_ENABLED_DEFAULT)) { + hadoopConfig.set(Constants.INPUT_STREAM_TYPE, Constants.INPUT_STREAM_TYPE_CLASSIC); + } + } + + @Override + protected void updateDelegationTokenConfig(org.apache.hadoop.conf.Configuration hadoopConfig) { + AbstractS3DelegationTokenReceiver.updateHadoopConfig( + hadoopConfig, HadoopDynamicTemporaryAWSCredentialsProvider.NAME); + } } diff --git a/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/token/S3HadoopDelegationTokenProvider.java b/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/token/S3HadoopDelegationTokenProvider.java index a6938546077991..d6d77fc87c04ad 100644 --- a/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/token/S3HadoopDelegationTokenProvider.java +++ b/flink-filesystems/flink-s3-fs-hadoop/src/main/java/org/apache/flink/fs/s3hadoop/token/S3HadoopDelegationTokenProvider.java @@ -20,12 +20,39 @@ import org.apache.flink.annotation.Internal; import org.apache.flink.fs.s3.common.token.AbstractS3DelegationTokenProvider; +import org.apache.flink.fs.s3.common.token.S3SessionCredentials; -/** Delegation token provider for S3 Hadoop filesystems. */ +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.Credentials; + +/** Delegation token provider for S3 Hadoop filesystems, based on AWS SDK v2. */ @Internal public class S3HadoopDelegationTokenProvider extends AbstractS3DelegationTokenProvider { + @Override public String serviceName() { return "s3-hadoop"; } + + @Override + protected S3SessionCredentials getSessionCredentials( + String region, String accessKey, String secretKey) { + try (StsClient stsClient = + StsClient.builder() + .region(Region.of(region)) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey))) + .build()) { + Credentials credentials = stsClient.getSessionToken().credentials(); + return new S3SessionCredentials( + credentials.accessKeyId(), + credentials.secretAccessKey(), + credentials.sessionToken(), + credentials.expiration().toEpochMilli()); + } + } } diff --git a/flink-filesystems/flink-s3-fs-hadoop/src/main/resources/META-INF/NOTICE b/flink-filesystems/flink-s3-fs-hadoop/src/main/resources/META-INF/NOTICE index 89e5c0c3fb1a24..c3338aab1278cf 100644 --- a/flink-filesystems/flink-s3-fs-hadoop/src/main/resources/META-INF/NOTICE +++ b/flink-filesystems/flink-s3-fs-hadoop/src/main/resources/META-INF/NOTICE @@ -3,56 +3,107 @@ Copyright 2014-2026 The Apache Software Foundation This project bundles the following dependencies under the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt) -- com.amazonaws:aws-java-sdk-core:1.12.779 -- com.amazonaws:aws-java-sdk-dynamodb:1.12.779 -- com.amazonaws:aws-java-sdk-kms:1.12.779 -- com.amazonaws:aws-java-sdk-s3:1.12.779 -- com.amazonaws:aws-java-sdk-sts:1.12.779 -- com.amazonaws:jmespath-java:1.12.779 - com.fasterxml.jackson.core:jackson-annotations:2.22 - com.fasterxml.jackson.core:jackson-core:2.22.2 - com.fasterxml.jackson.core:jackson-databind:2.22.2 -- com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.22.2 -- com.fasterxml.woodstox:woodstox-core:5.3.0 -- com.google.guava:failureaccess:1.0 -- com.google.guava:guava:27.0-jre +- com.fasterxml.woodstox:woodstox-core:5.4.0 +- com.google.guava:failureaccess:1.0.1 +- com.google.guava:guava:32.0.1-jre - com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava -- com.google.j2objc:j2objc-annotations:1.1 -- commons-beanutils:commons-beanutils:1.9.4 +- com.google.j2objc:j2objc-annotations:2.8 - commons-codec:commons-codec:1.15 -- commons-collections:commons-collections:3.2.2 - commons-io:commons-io:2.15.1 - commons-logging:commons-logging:1.1.3 -- joda-time:joda-time:2.5 +- io.dropwizard.metrics:metrics-core:3.2.5 +- io.netty:netty-buffer:4.2.17.Final +- io.netty:netty-codec:4.2.17.Final +- io.netty:netty-codec-base:4.2.17.Final +- io.netty:netty-codec-compression:4.2.17.Final +- io.netty:netty-codec-http:4.2.17.Final +- io.netty:netty-codec-http2:4.2.17.Final +- io.netty:netty-codec-marshalling:4.2.17.Final +- io.netty:netty-codec-protobuf:4.2.17.Final +- io.netty:netty-common:4.2.17.Final +- io.netty:netty-handler:4.2.17.Final +- io.netty:netty-resolver:4.2.17.Final +- io.netty:netty-transport:4.2.17.Final +- io.netty:netty-transport-classes-epoll:4.2.17.Final +- io.netty:netty-transport-native-epoll:4.2.17.Final +- io.netty:netty-transport-native-unix-common:4.2.17.Final +- org.apache.commons:commons-collections4:4.4 - org.apache.commons:commons-compress:1.26.0 -- org.apache.commons:commons-configuration2:2.1.1 +- org.apache.commons:commons-configuration2:2.10.1 - org.apache.commons:commons-lang3:3.18.0 - org.apache.commons:commons-text:1.10.0 -- org.apache.hadoop.thirdparty:hadoop-shaded-guava:1.1.1 -- org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_7:1.1.1 -- org.apache.hadoop:hadoop-annotations:3.3.4 -- org.apache.hadoop:hadoop-auth:3.3.4 -- org.apache.hadoop:hadoop-aws:3.3.4 -- org.apache.hadoop:hadoop-common:3.3.4 +- org.apache.hadoop.thirdparty:hadoop-shaded-guava:1.5.0 +- org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_25:1.5.0 +- org.apache.hadoop:hadoop-annotations:3.4.3 +- org.apache.hadoop:hadoop-auth:3.4.3 +- org.apache.hadoop:hadoop-aws:3.4.3 +- org.apache.hadoop:hadoop-common:3.4.3 - org.apache.httpcomponents:httpclient:4.5.13 - org.apache.httpcomponents:httpcore:4.4.14 -- org.apache.kerby:kerb-core:1.0.1 -- org.apache.kerby:kerby-asn1:1.0.1 -- org.apache.kerby:kerby-pkix:1.0.1 -- org.apache.kerby:kerby-util:1.0.1 -- org.wildfly.openssl:wildfly-openssl:1.0.7.Final +- org.apache.kerby:kerb-core:2.0.3 +- org.apache.kerby:kerb-crypto:2.0.3 +- org.apache.kerby:kerb-util:2.0.3 +- org.apache.kerby:kerby-asn1:2.0.3 +- org.apache.kerby:kerby-config:2.0.3 +- org.apache.kerby:kerby-pkix:2.0.3 +- org.apache.kerby:kerby-util:2.0.3 +- org.codehaus.jettison:jettison:1.5.4 +- org.wildfly.openssl:wildfly-openssl:2.2.5.Final - org.xerial.snappy:snappy-java:1.1.10.7 -- software.amazon.ion:ion-java:1.0.2 +- software.amazon.awssdk:annotations:2.44.4 +- software.amazon.awssdk:apache-client:2.44.4 +- software.amazon.awssdk:arns:2.44.4 +- software.amazon.awssdk:auth:2.44.4 +- software.amazon.awssdk:aws-core:2.44.4 +- software.amazon.awssdk:aws-json-protocol:2.44.4 +- software.amazon.awssdk:aws-query-protocol:2.44.4 +- software.amazon.awssdk:aws-xml-protocol:2.44.4 +- software.amazon.awssdk:checksums:2.44.4 +- software.amazon.awssdk:checksums-spi:2.44.4 +- software.amazon.awssdk:crt-core:2.44.4 +- software.amazon.awssdk:endpoints-spi:2.44.4 +- software.amazon.awssdk:http-auth:2.44.4 +- software.amazon.awssdk:http-auth-aws:2.44.4 +- software.amazon.awssdk:http-auth-aws-eventstream:2.44.4 +- software.amazon.awssdk:http-auth-spi:2.44.4 +- software.amazon.awssdk:http-client-spi:2.44.4 +- software.amazon.awssdk:identity-spi:2.44.4 +- software.amazon.awssdk:json-utils:2.44.4 +- software.amazon.awssdk:kms:2.44.4 +- software.amazon.awssdk:metrics-spi:2.44.4 +- software.amazon.awssdk:netty-nio-client:2.44.4 +- software.amazon.awssdk:profiles:2.44.4 +- software.amazon.awssdk:protocol-core:2.44.4 +- software.amazon.awssdk:regions:2.44.4 +- software.amazon.awssdk:retries:2.44.4 +- software.amazon.awssdk:retries-spi:2.44.4 +- software.amazon.awssdk:s3:2.44.4 +- software.amazon.awssdk:s3-transfer-manager:2.44.4 +- software.amazon.awssdk:sdk-core:2.44.4 +- software.amazon.awssdk:sso:2.44.4 +- software.amazon.awssdk:sts:2.44.4 +- software.amazon.awssdk:third-party-jackson-core:2.44.4 +- software.amazon.awssdk:utils:2.44.4 +- software.amazon.awssdk:utils-lite:2.44.4 +- software.amazon.eventstream:eventstream:1.0.1 +- software.amazon.s3.analyticsaccelerator:analyticsaccelerator-s3:1.3.1 This project bundles the following dependencies under BSD-2 License (https://opensource.org/licenses/BSD-2-Clause). See bundled license files for details. -- dnsjava:dnsjava:2.1.7 +- dnsjava:dnsjava:3.6.1 This project bundles the following dependencies under the MIT (https://opensource.org/licenses/MIT) -- org.checkerframework:checker-qual:2.5.2 -- org.codehaus.mojo:animal-sniffer-annotations:1.17 +- org.checkerframework:checker-qual:3.33.0 +- org.reactivestreams:reactive-streams:1.0.4 + +This project bundles the following dependencies under the Bouncy Castle License (https://www.bouncycastle.org/licence.html) + +- org.bouncycastle:bcprov-jdk18on:1.82 This project bundles the following dependencies under the CDDL 1.1 license. See bundled license files for details. @@ -73,10 +124,10 @@ The bundled Apache Hadoop Relocated (Shaded) Third-party Miscellaneous Libs org.apache.hadoop.thirdparty:hadoop-shaded-guava dependency bundles the following dependencies under the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt) -- com.google.guava:guava:30.1.1-jre +- com.google.guava:guava:33.4.8-jre The bundled Apache Hadoop Relocated (Shaded) Third-party Miscellaneous Libs -org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_7 dependency bundles the following dependencies under +org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_25 dependency bundles the following dependencies under the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt) -- com.google.protobuf:protobuf-java:3.7.1 +- com.google.protobuf:protobuf-java:3.25.5 diff --git a/flink-filesystems/flink-s3-fs-hadoop/src/test/java/org/apache/flink/fs/s3/common/token/HadoopDynamicTemporaryAWSCredentialsProviderTest.java b/flink-filesystems/flink-s3-fs-hadoop/src/test/java/org/apache/flink/fs/s3/common/token/HadoopDynamicTemporaryAWSCredentialsProviderTest.java new file mode 100644 index 00000000000000..204ea77968efef --- /dev/null +++ b/flink-filesystems/flink-s3-fs-hadoop/src/test/java/org/apache/flink/fs/s3/common/token/HadoopDynamicTemporaryAWSCredentialsProviderTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.fs.s3.common.token; + +import org.apache.flink.util.InstantiationUtil; + +import org.apache.hadoop.fs.s3a.auth.NoAwsCredentialsException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** Tests for {@link HadoopDynamicTemporaryAWSCredentialsProvider}. */ +class HadoopDynamicTemporaryAWSCredentialsProviderTest { + + private static final String ACCESS_KEY_ID = "testAccessKeyId"; + private static final String SECRET_ACCESS_KEY = "testSecretAccessKey"; + private static final String SESSION_TOKEN = "testSessionToken"; + private static final long EXPIRATION_EPOCH_MILLI = 1234567890L; + + @BeforeEach + void beforeEach() { + AbstractS3DelegationTokenReceiver.credentials = null; + } + + @AfterEach + void afterEach() { + AbstractS3DelegationTokenReceiver.credentials = null; + } + + @Test + void nameMustMatchClassName() { + // NAME is a string literal so that referencing it (e.g. when registering the provider in + // the Hadoop configuration) never class-loads this provider outside this plugin; this pins + // the literal to the actual class name, which users reference from + // fs.s3a.aws.credentials.provider. + assertThat(HadoopDynamicTemporaryAWSCredentialsProvider.NAME) + .isEqualTo(HadoopDynamicTemporaryAWSCredentialsProvider.class.getName()); + } + + @Test + void resolveCredentialsShouldThrowNoAwsCredentialsExceptionWhenNoCredentials() { + HadoopDynamicTemporaryAWSCredentialsProvider provider = + new HadoopDynamicTemporaryAWSCredentialsProvider(); + + assertThatThrownBy(provider::resolveCredentials) + .isInstanceOf(NoAwsCredentialsException.class); + } + + @Test + void resolveCredentialsShouldReturnSessionCredentialsWhenProvided() throws Exception { + HadoopDynamicTemporaryAWSCredentialsProvider provider = + new HadoopDynamicTemporaryAWSCredentialsProvider(); + S3SessionCredentials credentials = + new S3SessionCredentials( + ACCESS_KEY_ID, SECRET_ACCESS_KEY, SESSION_TOKEN, EXPIRATION_EPOCH_MILLI); + AbstractS3DelegationTokenReceiver receiver = + new AbstractS3DelegationTokenReceiver() { + @Override + public String serviceName() { + return "s3-hadoop"; + } + }; + + receiver.onNewTokensObtained(InstantiationUtil.serializeObject(credentials)); + + AwsCredentials resolved = provider.resolveCredentials(); + assertThat(resolved).isInstanceOf(AwsSessionCredentials.class); + AwsSessionCredentials sessionCredentials = (AwsSessionCredentials) resolved; + assertThat(sessionCredentials.accessKeyId()).isEqualTo(ACCESS_KEY_ID); + assertThat(sessionCredentials.secretAccessKey()).isEqualTo(SECRET_ACCESS_KEY); + assertThat(sessionCredentials.sessionToken()).isEqualTo(SESSION_TOKEN); + } +} diff --git a/flink-filesystems/flink-s3-fs-hadoop/src/test/java/org/apache/flink/fs/s3hadoop/HadoopS3FileSystemTest.java b/flink-filesystems/flink-s3-fs-hadoop/src/test/java/org/apache/flink/fs/s3hadoop/HadoopS3FileSystemTest.java index 8114cd4ee841a6..0d6dedf9c057d6 100644 --- a/flink-filesystems/flink-s3-fs-hadoop/src/test/java/org/apache/flink/fs/s3hadoop/HadoopS3FileSystemTest.java +++ b/flink-filesystems/flink-s3-fs-hadoop/src/test/java/org/apache/flink/fs/s3hadoop/HadoopS3FileSystemTest.java @@ -125,6 +125,58 @@ void testValueSuffixMappingForYamlCollisionAvoidance() { assertThat(hadoopConf.get("fs.s3a.s3guard.ddb.table.capacity.read", null)).isEqualTo("10"); } + // ------------------------------------------------------------------------ + // These tests check the Flink-side default for the S3A input stream type. + // hadoop-aws 3.4.3 defaults fs.s3a.input.stream.type to the analytics- + // accelerator stream; Flink pins the classic stream unless the user + // configures otherwise. + // ------------------------------------------------------------------------ + + /** With no user configuration, the classic input stream must be pinned. */ + @Test + void testInputStreamTypeDefaultsToClassic() { + org.apache.hadoop.conf.Configuration hadoopConf = + loadHadoopConfigWithDefaults(new Configuration()); + + assertThat(hadoopConf.get("fs.s3a.input.stream.type", null)).isEqualTo("classic"); + } + + /** An explicit user-configured stream type must not be overridden. */ + @Test + void testInputStreamTypeUserOverrideWins() { + Configuration conf = new Configuration(); + conf.setString("s3.input.stream.type", "analytics"); + + org.apache.hadoop.conf.Configuration hadoopConf = loadHadoopConfigWithDefaults(conf); + + assertThat(hadoopConf.get("fs.s3a.input.stream.type", null)).isEqualTo("analytics"); + } + + /** + * When the legacy prefetch toggle is enabled and no stream type is set, Hadoop selects the + * prefetching stream — pinning the classic stream would silently disable that opt-in. + */ + @Test + void testInputStreamTypeNotPinnedWhenPrefetchEnabled() { + Configuration conf = new Configuration(); + conf.setString("s3.prefetch.enabled", "true"); + + org.apache.hadoop.conf.Configuration hadoopConf = loadHadoopConfigWithDefaults(conf); + + assertThat(hadoopConf.get("fs.s3a.input.stream.type", null)).isNull(); + assertThat(hadoopConf.get("fs.s3a.prefetch.enabled", null)).isEqualTo("true"); + } + + private static org.apache.hadoop.conf.Configuration loadHadoopConfigWithDefaults( + Configuration flinkConf) { + HadoopConfigLoader configLoader = S3FileSystemFactory.createHadoopConfigLoader(); + configLoader.setFlinkConfig(flinkConf); + + org.apache.hadoop.conf.Configuration hadoopConf = configLoader.getOrLoadHadoopConfig(); + new S3FileSystemFactory().applyHadoopConfigDefaults(hadoopConf); + return hadoopConf; + } + private static void checkHadoopAccessKeys( Configuration flinkConf, String accessKey, String secretKey) { HadoopConfigLoader configLoader = S3FileSystemFactory.createHadoopConfigLoader(); diff --git a/flink-filesystems/flink-s3-fs-hadoop/src/test/java/org/apache/flink/fs/s3hadoop/token/S3HadoopDelegationTokenProviderTest.java b/flink-filesystems/flink-s3-fs-hadoop/src/test/java/org/apache/flink/fs/s3hadoop/token/S3HadoopDelegationTokenProviderTest.java new file mode 100644 index 00000000000000..3380998d73157e --- /dev/null +++ b/flink-filesystems/flink-s3-fs-hadoop/src/test/java/org/apache/flink/fs/s3hadoop/token/S3HadoopDelegationTokenProviderTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.fs.s3hadoop.token; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** Tests for {@link S3HadoopDelegationTokenProvider}. */ +class S3HadoopDelegationTokenProviderTest { + + @Test + void serviceNameIsS3Hadoop() { + assertThat(new S3HadoopDelegationTokenProvider().serviceName()).isEqualTo("s3-hadoop"); + } + + @Test + void serviceConfigPrefixIsDerivedFromConfigPrefixAndServiceName() { + assertThat(new S3HadoopDelegationTokenProvider().serviceConfigPrefix()) + .isEqualTo("security.delegation.token.provider.s3-hadoop"); + } +} diff --git a/flink-filesystems/flink-s3-fs-presto/pom.xml b/flink-filesystems/flink-s3-fs-presto/pom.xml index cb3236ce1a67c0..03f6ce0f74f4c4 100644 --- a/flink-filesystems/flink-s3-fs-presto/pom.xml +++ b/flink-filesystems/flink-s3-fs-presto/pom.xml @@ -56,13 +56,44 @@ under the License. flink-architecture-tests-test test - + org.apache.flink flink-s3-fs-base ${project.version} ${flink.markBundledAsOptional} + + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + sts + + + software.amazon.awssdk + kms + + + software.amazon.awssdk + dynamodb + + + software.amazon.awssdk + sso + + + software.amazon.awssdk + auth + + + software.amazon.awssdk + s3-transfer-manager + + @@ -523,14 +554,6 @@ under the License. - - - - commons-beanutils - commons-beanutils - 1.9.4 - ${flink.markBundledAsOptional} - @@ -683,6 +706,14 @@ under the License. META-INF/services/javax.xml.stream.* META-INF/LICENSE.txt LICENSE + + NOTICE + THIRD-PARTY-NOTICES diff --git a/flink-filesystems/flink-s3-fs-presto/src/main/java/org/apache/flink/fs/s3presto/token/S3PrestoDelegationTokenProvider.java b/flink-filesystems/flink-s3-fs-presto/src/main/java/org/apache/flink/fs/s3presto/token/S3PrestoDelegationTokenProvider.java index 81fc5e0e301c3e..70f059c017c47e 100644 --- a/flink-filesystems/flink-s3-fs-presto/src/main/java/org/apache/flink/fs/s3presto/token/S3PrestoDelegationTokenProvider.java +++ b/flink-filesystems/flink-s3-fs-presto/src/main/java/org/apache/flink/fs/s3presto/token/S3PrestoDelegationTokenProvider.java @@ -20,12 +20,42 @@ import org.apache.flink.annotation.Internal; import org.apache.flink.fs.s3.common.token.AbstractS3DelegationTokenProvider; +import org.apache.flink.fs.s3.common.token.S3SessionCredentials; -/** Delegation token provider for S3 Presto filesystems. */ +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.securitytoken.AWSSecurityTokenService; +import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; +import com.amazonaws.services.securitytoken.model.Credentials; + +/** Delegation token provider for S3 Presto filesystems, based on AWS SDK v1. */ @Internal public class S3PrestoDelegationTokenProvider extends AbstractS3DelegationTokenProvider { + @Override public String serviceName() { return "s3-presto"; } + + @Override + protected S3SessionCredentials getSessionCredentials( + String region, String accessKey, String secretKey) { + AWSSecurityTokenService stsClient = + AWSSecurityTokenServiceClientBuilder.standard() + .withRegion(region) + .withCredentials( + new AWSStaticCredentialsProvider( + new BasicAWSCredentials(accessKey, secretKey))) + .build(); + try { + Credentials credentials = stsClient.getSessionToken().getCredentials(); + return new S3SessionCredentials( + credentials.getAccessKeyId(), + credentials.getSecretAccessKey(), + credentials.getSessionToken(), + credentials.getExpiration().getTime()); + } finally { + stsClient.shutdown(); + } + } } diff --git a/flink-filesystems/flink-s3-fs-presto/src/main/resources/META-INF/NOTICE b/flink-filesystems/flink-s3-fs-presto/src/main/resources/META-INF/NOTICE index 111f291a6996b4..0427f06540e98b 100644 --- a/flink-filesystems/flink-s3-fs-presto/src/main/resources/META-INF/NOTICE +++ b/flink-filesystems/flink-s3-fs-presto/src/main/resources/META-INF/NOTICE @@ -26,52 +26,68 @@ This project bundles the following dependencies under the Apache Software Licens - com.fasterxml.jackson.core:jackson-core:2.22.2 - com.fasterxml.jackson.core:jackson-databind:2.22.2 - com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.22.2 -- com.fasterxml.woodstox:woodstox-core:5.3.0 +- com.fasterxml.woodstox:woodstox-core:5.4.0 - com.google.guava:guava:32.1.0-jre - com.google.guava:failureaccess:1.0.1 - com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - com.google.inject:guice:5.1.0 -- commons-beanutils:commons-beanutils:1.9.4 - commons-codec:commons-codec:1.15 -- commons-collections:commons-collections:3.2.2 - commons-io:commons-io:2.15.1 - commons-logging:commons-logging:1.1.3 - io.airlift:slice:0.38 - io.airlift:units:1.3 +- io.dropwizard.metrics:metrics-core:3.2.5 +- io.netty:netty-buffer:4.2.17.Final +- io.netty:netty-codec-base:4.2.17.Final +- io.netty:netty-common:4.2.17.Final +- io.netty:netty-handler:4.2.17.Final +- io.netty:netty-resolver:4.2.17.Final +- io.netty:netty-transport:4.2.17.Final +- io.netty:netty-transport-classes-epoll:4.2.17.Final +- io.netty:netty-transport-native-epoll:4.2.17.Final +- io.netty:netty-transport-native-unix-common:4.2.17.Final - joda-time:joda-time:2.5 -- org.alluxio:alluxio-shaded-client:2.8.1 +- org.apache.commons:commons-collections4:4.4 - org.apache.commons:commons-compress:1.26.0 -- org.apache.commons:commons-configuration2:2.1.1 +- org.apache.commons:commons-configuration2:2.10.1 - org.apache.commons:commons-lang3:3.18.0 - org.apache.commons:commons-text:1.10.0 -- org.apache.hadoop.thirdparty:hadoop-shaded-guava:1.1.1 -- org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_7:1.1.1 -- org.apache.hadoop:hadoop-annotations:3.3.4 -- org.apache.hadoop:hadoop-auth:3.3.4 -- org.apache.hadoop:hadoop-aws:3.3.4 -- org.apache.hadoop:hadoop-common:3.3.4 +- org.apache.hadoop.thirdparty:hadoop-shaded-guava:1.5.0 +- org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_25:1.5.0 +- org.apache.hadoop:hadoop-annotations:3.4.3 +- org.apache.hadoop:hadoop-auth:3.4.3 +- org.apache.hadoop:hadoop-aws:3.4.3 +- org.apache.hadoop:hadoop-common:3.4.3 - org.apache.httpcomponents:httpclient:4.5.13 - org.apache.httpcomponents:httpcore:4.4.14 - org.apache.hudi:hudi-presto-bundle:0.14.0 -- org.apache.kerby:kerb-core:1.0.1 -- org.apache.kerby:kerby-asn1:1.0.1 -- org.apache.kerby:kerby-pkix:1.0.1 -- org.apache.kerby:kerby-util:1.0.1 +- org.apache.kerby:kerb-core:2.0.3 +- org.apache.kerby:kerb-crypto:2.0.3 +- org.apache.kerby:kerb-util:2.0.3 +- org.apache.kerby:kerby-asn1:2.0.3 +- org.apache.kerby:kerby-config:2.0.3 +- org.apache.kerby:kerby-pkix:2.0.3 +- org.apache.kerby:kerby-util:2.0.3 +- org.codehaus.jettison:jettison:1.5.4 - org.weakref:jmxutils:1.19 -- org.wildfly.openssl:wildfly-openssl:1.0.7.Final +- org.wildfly.openssl:wildfly-openssl:2.2.5.Final - org.xerial.snappy:snappy-java:1.1.10.7 -- software.amazon.ion:ion-java:1.0.2 +- software.amazon.s3.analyticsaccelerator:analyticsaccelerator-s3:1.3.1 This project bundles the following dependencies under BSD-2 License (https://opensource.org/licenses/BSD-2-Clause). See bundled license files for details. -- dnsjava:dnsjava:2.1.7 +- dnsjava:dnsjava:3.6.1 This project bundles the following dependencies under the Creative Commons CC0 1.0 Universal Public Domain Dedication License (http://creativecommons.org/publicdomain/zero/1.0/) See bundled license files for details. - org.hdrhistogram:HdrHistogram:2.1.9 +This project bundles the following dependencies under the Bouncy Castle License (https://www.bouncycastle.org/licence.html) + +- org.bouncycastle:bcprov-jdk18on:1.82 + This project bundles the following dependencies under the CDDL 1.1 license. See bundled license files for details. @@ -96,10 +112,10 @@ The bundled Apache Hadoop Relocated (Shaded) Third-party Miscellaneous Libs org.apache.hadoop.thirdparty:hadoop-shaded-guava dependency bundles the following dependencies under the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt) -- com.google.guava:guava:30.1.1-jre +- com.google.guava:guava:33.4.8-jre The bundled Apache Hadoop Relocated (Shaded) Third-party Miscellaneous Libs -org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_7 dependency bundles the following dependencies under +org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_25 dependency bundles the following dependencies under the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt) -- com.google.protobuf:protobuf-java:3.7.1 +- com.google.protobuf:protobuf-java:3.25.5 diff --git a/flink-filesystems/flink-s3-fs-presto/src/test/java/org/apache/flink/fs/s3presto/token/S3PrestoDelegationTokenProviderTest.java b/flink-filesystems/flink-s3-fs-presto/src/test/java/org/apache/flink/fs/s3presto/token/S3PrestoDelegationTokenProviderTest.java new file mode 100644 index 00000000000000..bf03a78b9df955 --- /dev/null +++ b/flink-filesystems/flink-s3-fs-presto/src/test/java/org/apache/flink/fs/s3presto/token/S3PrestoDelegationTokenProviderTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.fs.s3presto.token; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** Tests for {@link S3PrestoDelegationTokenProvider}. */ +class S3PrestoDelegationTokenProviderTest { + + @Test + void serviceNameIsS3Presto() { + assertThat(new S3PrestoDelegationTokenProvider().serviceName()).isEqualTo("s3-presto"); + } + + @Test + void serviceConfigPrefixIsDerivedFromConfigPrefixAndServiceName() { + assertThat(new S3PrestoDelegationTokenProvider().serviceConfigPrefix()) + .isEqualTo("security.delegation.token.provider.s3-presto"); + } +} diff --git a/flink-filesystems/pom.xml b/flink-filesystems/pom.xml index a845f7676e7fa5..f954b1e5643ca9 100644 --- a/flink-filesystems/pom.xml +++ b/flink-filesystems/pom.xml @@ -34,7 +34,7 @@ under the License. pom - 3.3.4 + 3.4.3