From 1db9b86d25805af49017e5736bdb37f3d1a9dd52 Mon Sep 17 00:00:00 2001 From: Andrey Yarovoy Date: Wed, 4 Nov 2026 15:22:24 -0500 Subject: [PATCH 1/3] HDDS-17707. Remove AWS SDK for Java v1 Co-authored-by: Claude Opus 5.5 --- .../dist/src/main/license/bin/LICENSE.txt | 35 +- .../dist/src/main/license/bin/NOTICE.txt | 30 + .../dist/src/main/license/jar-report.txt | 45 +- .../dist/src/shell/conf/log4j.properties | 4 +- hadoop-ozone/freon/pom.xml | 51 +- ...va => FreonS3TraceContextInterceptor.java} | 19 +- .../hadoop/ozone/freon/S3BucketGenerator.java | 2 +- .../hadoop/ozone/freon/S3EntityGenerator.java | 40 +- .../hadoop/ozone/freon/S3KeyGenerator.java | 53 +- .../TestFreonS3TraceContextInterceptor.java | 73 + hadoop-ozone/integration-test-s3/pom.xml | 10 - .../hadoop/ozone/s3/S3ClientFactory.java | 60 - .../ozone/s3/awssdk/OzoneS3SDKTests.java | 9 - .../s3/awssdk/v1/AbstractS3SDKV1Tests.java | 3167 ----------------- .../s3/awssdk/v2/AbstractS3SDKV2Tests.java | 481 +++ .../src/test/resources/log4j.properties | 3 - pom.xml | 46 +- 17 files changed, 776 insertions(+), 3352 deletions(-) rename hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/{FreonS3TraceContextRequestHandler.java => FreonS3TraceContextInterceptor.java} (64%) create mode 100644 hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonS3TraceContextInterceptor.java delete mode 100644 hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java diff --git a/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt b/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt index 5dab10019565..b8c36618f2a1 100644 --- a/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt +++ b/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt @@ -266,15 +266,10 @@ Apache License 2.0 ===================== ch.qos.reload4j:reload4j - com.amazonaws:aws-java-sdk-core - com.amazonaws:aws-java-sdk-kms - com.amazonaws:aws-java-sdk-s3 - com.amazonaws:jmespath-java com.carrotsearch:hppc com.fasterxml.jackson.core:jackson-annotations com.fasterxml.jackson.core:jackson-core com.fasterxml.jackson.core:jackson-databind - com.fasterxml.jackson.dataformat:jackson-dataformat-cbor com.fasterxml.jackson.dataformat:jackson-dataformat-xml com.fasterxml.jackson.datatype:jackson-datatype-jsr310 com.fasterxml.jackson.jaxrs:jackson-jaxrs-base @@ -488,6 +483,36 @@ Apache License 2.0 org.xerial.snappy:snappy-java org.xerial:sqlite-jdbc org.yaml:snakeyaml + software.amazon.awssdk:annotations + software.amazon.awssdk:apache-client + software.amazon.awssdk:arns + software.amazon.awssdk:auth + software.amazon.awssdk:aws-core + software.amazon.awssdk:aws-query-protocol + software.amazon.awssdk:aws-xml-protocol + software.amazon.awssdk:checksums + software.amazon.awssdk:checksums-spi + software.amazon.awssdk:crt-core + software.amazon.awssdk:endpoints-spi + software.amazon.awssdk:http-auth + software.amazon.awssdk:http-auth-aws + software.amazon.awssdk:http-auth-aws-eventstream + software.amazon.awssdk:http-auth-spi + software.amazon.awssdk:http-client-spi + software.amazon.awssdk:identity-spi + software.amazon.awssdk:json-utils + software.amazon.awssdk:metrics-spi + software.amazon.awssdk:profiles + software.amazon.awssdk:protocol-core + software.amazon.awssdk:regions + software.amazon.awssdk:retries + software.amazon.awssdk:retries-spi + software.amazon.awssdk:s3 + software.amazon.awssdk:sdk-core + software.amazon.awssdk:third-party-jackson-core + software.amazon.awssdk:utils + software.amazon.awssdk:utils-lite + software.amazon.eventstream:eventstream MIT ===================== diff --git a/hadoop-ozone/dist/src/main/license/bin/NOTICE.txt b/hadoop-ozone/dist/src/main/license/bin/NOTICE.txt index dd78c02888cf..c5c1fda48840 100644 --- a/hadoop-ozone/dist/src/main/license/bin/NOTICE.txt +++ b/hadoop-ozone/dist/src/main/license/bin/NOTICE.txt @@ -448,6 +448,36 @@ This product includes software developed at The Apache Software Foundation (http://www.apache.org/). +software.amazon.awssdk +==================== + +AWS SDK for Java 2.0 +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed by +Amazon Technologies, Inc (http://www.amazon.com/). + +********************** +THIRD PARTY COMPONENTS +********************** +This software includes third party software subject to the following copyrights: +- XML parsing and utility functions from JetS3t - Copyright 2006-2009 James Murty. +- PKCS#1 PEM encoded private key parsing and utility functions from oauth.googlecode.com - Copyright 1998-2010 AOL Inc. +- Apache Commons Lang - https://github.com/apache/commons-lang +- Netty Reactive Streams - https://github.com/playframework/netty-reactive-streams +- Jackson-core - https://github.com/FasterXML/jackson-core +- Jackson-dataformat-cbor - https://github.com/FasterXML/jackson-dataformats-binary + +The licenses for these third party components are included in LICENSE.txt + +- For Apache Commons Lang see also this required NOTICE: + Apache Commons Lang + Copyright 2001-2020 The Apache Software Foundation + + This product includes software developed at + The Apache Software Foundation (https://www.apache.org/). + + ==================== ratis-thirdparty-misc is a shaded dependency which includes additional 3rd party dependencies in shaded form. For the detailed list of the dependencies and the associated NOTICE file see licenses/NOTICE-ratis-thirdparty-misc.txt. diff --git a/hadoop-ozone/dist/src/main/license/jar-report.txt b/hadoop-ozone/dist/src/main/license/jar-report.txt index 886623bde38f..28874de0aa07 100644 --- a/hadoop-ozone/dist/src/main/license/jar-report.txt +++ b/hadoop-ozone/dist/src/main/license/jar-report.txt @@ -1,26 +1,34 @@ share/ozone/lib/aircompressor.jar +share/ozone/lib/aircompressor.jar share/ozone/lib/animal-sniffer-annotations.jar share/ozone/lib/annotations.jar share/ozone/lib/annotations.jar +share/ozone/lib/annotations.jar +share/ozone/lib/annotations.jar share/ozone/lib/aopalliance-repackaged.jar share/ozone/lib/aopalliance.jar +share/ozone/lib/apache-client.jar share/ozone/lib/apache-log4j-extras.jar +share/ozone/lib/arns.jar share/ozone/lib/asm-analysis.jar share/ozone/lib/asm-commons.jar share/ozone/lib/asm-tree.jar share/ozone/lib/asm-util.jar share/ozone/lib/asm.jar share/ozone/lib/aspectjrt.jar +share/ozone/lib/auth.jar share/ozone/lib/avro.jar -share/ozone/lib/aws-java-sdk-core.jar -share/ozone/lib/aws-java-sdk-kms.jar -share/ozone/lib/aws-java-sdk-s3.jar +share/ozone/lib/aws-core.jar +share/ozone/lib/aws-query-protocol.jar +share/ozone/lib/aws-xml-protocol.jar share/ozone/lib/bcpkix-jdk18on.jar share/ozone/lib/bcprov-jdk18on.jar share/ozone/lib/bcutil-jdk18on.jar share/ozone/lib/bonecp.RELEASE.jar share/ozone/lib/caffeine.jar share/ozone/lib/cdi-api.SP1.jar +share/ozone/lib/checksums-spi.jar +share/ozone/lib/checksums.jar share/ozone/lib/commons-beanutils.jar share/ozone/lib/commons-cli.jar share/ozone/lib/commons-codec.jar @@ -38,12 +46,15 @@ share/ozone/lib/commons-pool2.jar share/ozone/lib/commons-text.jar share/ozone/lib/commons-validator.jar share/ozone/lib/converter-jackson.jar +share/ozone/lib/crt-core.jar share/ozone/lib/curator-client.jar share/ozone/lib/curator-framework.jar share/ozone/lib/derby.jar share/ozone/lib/disruptor.jar share/ozone/lib/dnsjava.jar +share/ozone/lib/endpoints-spi.jar share/ozone/lib/error_prone_annotations.jar +share/ozone/lib/eventstream.jar share/ozone/lib/failureaccess.jar share/ozone/lib/grpc-api.jar share/ozone/lib/grpc-context.jar @@ -84,6 +95,11 @@ share/ozone/lib/hk2-api.jar share/ozone/lib/hk2-locator.jar share/ozone/lib/hk2-utils.jar share/ozone/lib/hppc.jar +share/ozone/lib/http-auth-aws-eventstream.jar +share/ozone/lib/http-auth-aws.jar +share/ozone/lib/http-auth-spi.jar +share/ozone/lib/http-auth.jar +share/ozone/lib/http-client-spi.jar share/ozone/lib/httpclient.jar share/ozone/lib/httpcore.jar share/ozone/lib/iceberg-api.jar @@ -92,12 +108,12 @@ share/ozone/lib/iceberg-common.jar share/ozone/lib/iceberg-core.jar share/ozone/lib/iceberg-orc.jar share/ozone/lib/iceberg-parquet.jar +share/ozone/lib/identity-spi.jar share/ozone/lib/istack-commons-runtime.jar share/ozone/lib/j2objc-annotations.jar share/ozone/lib/jackson-annotations.jar share/ozone/lib/jackson-core.jar share/ozone/lib/jackson-databind.jar -share/ozone/lib/jackson-dataformat-cbor.jar share/ozone/lib/jackson-dataformat-xml.jar share/ozone/lib/jackson-datatype-jsr310.jar share/ozone/lib/jackson-jaxrs-base.jar @@ -105,6 +121,7 @@ share/ozone/lib/jackson-jaxrs-json-provider.jar share/ozone/lib/jackson-module-jaxb-annotations.jar share/ozone/lib/jakarta.activation-api.jar share/ozone/lib/jakarta.activation.jar +share/ozone/lib/jakarta.activation.jar share/ozone/lib/jakarta.annotation-api.jar share/ozone/lib/jakarta.inject-api.jar share/ozone/lib/jakarta.inject.jar @@ -161,7 +178,6 @@ share/ozone/lib/jgrapht-ext.jar share/ozone/lib/jgraphx.jar share/ozone/lib/jheaps.jar share/ozone/lib/jline.jar -share/ozone/lib/jmespath-java.jar share/ozone/lib/jnr-a64asm.jar share/ozone/lib/jnr-constants.jar share/ozone/lib/jnr-ffi.jar @@ -172,12 +188,13 @@ share/ozone/lib/jooq-codegen.jar share/ozone/lib/jooq-meta.jar share/ozone/lib/jooq.jar share/ozone/lib/jsch.jar +share/ozone/lib/json-utils.jar share/ozone/lib/jsp-api.jar share/ozone/lib/jspecify.jar share/ozone/lib/jsr311-api.jar +share/ozone/lib/jtokkit.jar share/ozone/lib/jts-core.jar share/ozone/lib/jul-to-slf4j.jar -share/ozone/lib/jtokkit.jar share/ozone/lib/kerb-core.jar share/ozone/lib/kerb-crypto.jar share/ozone/lib/kerb-util.jar @@ -196,6 +213,7 @@ share/ozone/lib/listenablefuture-empty-to-avoid-conflict-with-guava.jar share/ozone/lib/log4j-api.jar share/ozone/lib/log4j-core.jar share/ozone/lib/metrics-core.jar +share/ozone/lib/metrics-spi.jar share/ozone/lib/netty-buffer.Final.jar share/ozone/lib/netty-codec-http.Final.jar share/ozone/lib/netty-codec-http2.Final.jar @@ -221,6 +239,7 @@ share/ozone/lib/okhttp-jvm.jar share/ozone/lib/okhttp-sse.jar share/ozone/lib/okhttp.jar share/ozone/lib/okio-jvm.jar +share/ozone/lib/okio-jvm.jar share/ozone/lib/okio.jar share/ozone/lib/openai4j.jar share/ozone/lib/opentelemetry-api.jar @@ -276,12 +295,14 @@ share/ozone/lib/parquet-variant.jar share/ozone/lib/perfmark-api.jar share/ozone/lib/picocli-shell-jline3.jar share/ozone/lib/picocli.jar +share/ozone/lib/profiles.jar share/ozone/lib/proto-google-common-protos.jar share/ozone/lib/protobuf-java.jar +share/ozone/lib/protocol-core.jar +share/ozone/lib/r2dbc-spi.RELEASE.jar share/ozone/lib/ranger-audit-core.jar share/ozone/lib/ranger-authz-api.jar share/ozone/lib/ranger-common-utils.jar -share/ozone/lib/r2dbc-spi.RELEASE.jar share/ozone/lib/ranger-intg.jar share/ozone/lib/ranger-plugin-classloader.jar share/ozone/lib/ranger-plugins-common.jar @@ -300,11 +321,17 @@ share/ozone/lib/ratis-thirdparty-misc.jar share/ozone/lib/ratis-tools.jar share/ozone/lib/re2j.jar share/ozone/lib/reactive-streams.jar +share/ozone/lib/reactive-streams.jar share/ozone/lib/reflections.jar +share/ozone/lib/regions.jar share/ozone/lib/reload4j.jar +share/ozone/lib/retries-spi.jar +share/ozone/lib/retries.jar share/ozone/lib/retrofit.jar share/ozone/lib/rocksdb-checkpoint-differ.jar share/ozone/lib/rocksdbjni.jar +share/ozone/lib/s3.jar +share/ozone/lib/sdk-core.jar share/ozone/lib/simpleclient.jar share/ozone/lib/simpleclient_common.jar share/ozone/lib/simpleclient_dropwizard.jar @@ -318,8 +345,12 @@ share/ozone/lib/spring-jdbc.jar share/ozone/lib/spring-tx.jar share/ozone/lib/sqlite-jdbc.jar share/ozone/lib/stax2-api.jar +share/ozone/lib/stax2-api.jar +share/ozone/lib/third-party-jackson-core.jar share/ozone/lib/txw2.jar share/ozone/lib/ugsync-util.jar +share/ozone/lib/utils-lite.jar +share/ozone/lib/utils.jar share/ozone/lib/vault-java-driver.jar share/ozone/lib/weld-servlet-shaded.Final.jar share/ozone/lib/woodstox-core.jar diff --git a/hadoop-ozone/dist/src/shell/conf/log4j.properties b/hadoop-ozone/dist/src/shell/conf/log4j.properties index 5ed63ffd1ee5..eaa4f1343150 100644 --- a/hadoop-ozone/dist/src/shell/conf/log4j.properties +++ b/hadoop-ozone/dist/src/shell/conf/log4j.properties @@ -167,9 +167,7 @@ log4j.logger.http.requests=${ozone.http.request.logger} #log4j.logger.http.requests.webhdfs=INFO,HttpAccess # Custom Logging levels -# AWS SDK & S3A FileSystem -#log4j.logger.com.amazonaws=ERROR -log4j.logger.com.amazonaws.http.AmazonHttpClient=ERROR +# S3A FileSystem #log4j.logger.org.apache.hadoop.fs.s3a.S3AFileSystem=WARN # Log levels of third-party libraries diff --git a/hadoop-ozone/freon/pom.xml b/hadoop-ozone/freon/pom.xml index aed70f18d5f4..7b329a3bf827 100644 --- a/hadoop-ozone/freon/pom.xml +++ b/hadoop-ozone/freon/pom.xml @@ -31,14 +31,6 @@ - - com.amazonaws - aws-java-sdk-core - - - com.amazonaws - aws-java-sdk-s3 - com.fasterxml.jackson.core jackson-annotations @@ -139,6 +131,49 @@ org.slf4j slf4j-api + + software.amazon.awssdk + apache-client + + + software.amazon.awssdk + auth + + + software.amazon.awssdk + aws-core + + + software.amazon.awssdk + http-client-spi + + + software.amazon.awssdk + regions + + + software.amazon.awssdk + s3 + + + + software.amazon.awssdk + apache5-client + + + software.amazon.awssdk + netty-nio-client + + + + + software.amazon.awssdk + sdk-core + + + software.amazon.awssdk + utils + org.apache.ozone hdds-annotation-processing diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonS3TraceContextRequestHandler.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonS3TraceContextInterceptor.java similarity index 64% rename from hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonS3TraceContextRequestHandler.java rename to hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonS3TraceContextInterceptor.java index a5f40c6412b0..58da089cb0f1 100644 --- a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonS3TraceContextRequestHandler.java +++ b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonS3TraceContextInterceptor.java @@ -17,26 +17,29 @@ package org.apache.hadoop.ozone.freon; -import com.amazonaws.Request; -import com.amazonaws.handlers.RequestHandler2; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.Context; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.http.SdkHttpRequest; /** * Adds W3C trace context headers to each outgoing S3 request so the S3 Gateway * can attach its spans to the Freon task span, using {@link W3CTraceContextPropagator}. */ -public final class FreonS3TraceContextRequestHandler extends RequestHandler2 { +public final class FreonS3TraceContextInterceptor implements ExecutionInterceptor { @Override - public void beforeRequest(Request request) { + public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { if (!Span.current().getSpanContext().isValid()) { - return; + return context.httpRequest(); } + final SdkHttpRequest.Builder request = context.httpRequest().toBuilder(); W3CTraceContextPropagator.getInstance().inject( - Context.current(), + io.opentelemetry.context.Context.current(), request, - (carrier, key, value) -> carrier.addHeader(key, value)); + (carrier, key, value) -> carrier.putHeader(key, value)); + return request.build(); } } diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3BucketGenerator.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3BucketGenerator.java index f3ebccce24f8..e6ba1c5ad3d9 100644 --- a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3BucketGenerator.java +++ b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3BucketGenerator.java @@ -68,7 +68,7 @@ public Void call() throws Exception { private void createBucket(long bucketNum) throws Exception { String bName = getPrefix() + bucketNum; timer.time(() -> { - getS3().createBucket(bName); + getS3().createBucket(r -> r.bucket(bName)); return null; }); } diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java index bc8660489876..fab89c68fecc 100644 --- a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java +++ b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java @@ -17,12 +17,15 @@ package org.apache.hadoop.ozone.freon; -import com.amazonaws.auth.EnvironmentVariableCredentialsProvider; -import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import java.net.URI; import picocli.CommandLine; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.core.checksums.RequestChecksumCalculation; +import software.amazon.awssdk.core.checksums.ResponseChecksumValidation; +import software.amazon.awssdk.http.apache.ApacheHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3ClientBuilder; /** * Initiliazing common aspects of S3BucketGenerator and S3KeyGenerator. @@ -33,33 +36,36 @@ public class S3EntityGenerator extends BaseFreonGenerator { defaultValue = "http://localhost:9878") private String endpoint; - private AmazonS3 s3; + private S3Client s3; protected void s3ClientInit() { init(); - AmazonS3ClientBuilder amazonS3ClientBuilder = - AmazonS3ClientBuilder.standard() - .withCredentials(new EnvironmentVariableCredentialsProvider()); + S3ClientBuilder s3ClientBuilder = S3Client.builder() + .httpClientBuilder(ApacheHttpClient.builder()) + .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + // checksums only where S3 requires them: a load generator should not spend its time computing them + .requestChecksumCalculation(RequestChecksumCalculation.WHEN_REQUIRED) + .responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED) + .overrideConfiguration(c -> c.addExecutionInterceptor(new FreonS3TraceContextInterceptor())); if (!endpoint.isEmpty()) { - amazonS3ClientBuilder - .withPathStyleAccessEnabled(true) - .withEndpointConfiguration( - new EndpointConfiguration(endpoint, "us-east-1")); + s3ClientBuilder + .forcePathStyle(true) + .endpointOverride(URI.create(endpoint)) + .region(Region.US_EAST_1); } else { - amazonS3ClientBuilder.withRegion(Regions.DEFAULT_REGION); + s3ClientBuilder.region(Region.US_WEST_2); } - amazonS3ClientBuilder.withRequestHandlers(new FreonS3TraceContextRequestHandler()); - s3 = amazonS3ClientBuilder.build(); + s3 = s3ClientBuilder.build(); } public String getEndpoint() { return endpoint; } - public AmazonS3 getS3() { + public S3Client getS3() { return s3; } } diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java index 05bb75100192..6f04181f3399 100644 --- a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java +++ b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java @@ -19,14 +19,7 @@ import static org.apache.hadoop.ozone.OzoneConsts.OM_MULTIPART_MIN_SIZE; -import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest; -import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest; -import com.amazonaws.services.s3.model.InitiateMultipartUploadResult; -import com.amazonaws.services.s3.model.PartETag; -import com.amazonaws.services.s3.model.UploadPartRequest; -import com.amazonaws.services.s3.model.UploadPartResult; import com.codahale.metrics.Timer; -import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -38,6 +31,9 @@ import org.slf4j.LoggerFactory; import picocli.CommandLine.Command; import picocli.CommandLine.Option; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.model.CompletedPart; +import software.amazon.awssdk.services.s3.model.UploadPartResponse; /** * Generate random keys via the s3 interface. @@ -84,7 +80,7 @@ public class S3KeyGenerator extends S3EntityGenerator private Timer timer; - private String content; + private byte[] content; @Override public Void call() throws Exception { @@ -96,11 +92,10 @@ public Void call() throws Exception { s3ClientInit(); - content = RandomStringUtils.secure().nextAscii(fileSize); + content = RandomStringUtils.secure().nextAscii(fileSize).getBytes(StandardCharsets.UTF_8); timer = getMetrics().timer("key-create"); - System.setProperty("com.amazonaws.services.s3.disablePutObjectMD5Validation", "true"); runTests(this::createKey); return null; @@ -111,38 +106,24 @@ private void createKey(long counter) throws Exception { if (multiPart) { final String keyName = generateObjectName(counter); - final InitiateMultipartUploadRequest initiateRequest = - new InitiateMultipartUploadRequest(bucketName, keyName); + final String uploadId = getS3().createMultipartUpload(r -> r.bucket(bucketName).key(keyName)).uploadId(); - final InitiateMultipartUploadResult initiateMultipartUploadResult = - getS3().initiateMultipartUpload(initiateRequest); - final String uploadId = initiateMultipartUploadResult.getUploadId(); - - List parts = new ArrayList<>(); + List parts = new ArrayList<>(); for (int i = 1; i <= numberOfParts; i++) { - - final UploadPartRequest uploadPartRequest = new UploadPartRequest() - .withBucketName(bucketName) - .withKey(keyName) - .withPartNumber(i) - .withLastPart(i == numberOfParts) - .withUploadId(uploadId) - .withPartSize(fileSize) - .withInputStream(new ByteArrayInputStream(content.getBytes( - StandardCharsets.UTF_8))); - - final UploadPartResult uploadPartResult = - getS3().uploadPart(uploadPartRequest); - parts.add(uploadPartResult.getPartETag()); + final int partNumber = i; + final UploadPartResponse uploadPartResponse = getS3().uploadPart( + r -> r.bucket(bucketName).key(keyName).uploadId(uploadId).partNumber(partNumber) + .contentLength((long) content.length), + RequestBody.fromBytes(content)); + parts.add(CompletedPart.builder().partNumber(partNumber).eTag(uploadPartResponse.eTag()).build()); } - getS3().completeMultipartUpload( - new CompleteMultipartUploadRequest(bucketName, keyName, uploadId, - parts)); + getS3().completeMultipartUpload(r -> r.bucket(bucketName).key(keyName).uploadId(uploadId) + .multipartUpload(m -> m.parts(parts))); } else { - getS3().putObject(bucketName, generateObjectName(counter), - content); + getS3().putObject(r -> r.bucket(bucketName).key(generateObjectName(counter)), + RequestBody.fromBytes(content)); } return null; diff --git a/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonS3TraceContextInterceptor.java b/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonS3TraceContextInterceptor.java new file mode 100644 index 000000000000..8cb6b8199d1f --- /dev/null +++ b/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonS3TraceContextInterceptor.java @@ -0,0 +1,73 @@ +/* + * 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.hadoop.ozone.freon; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Scope; +import java.net.URI; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; + +/** Tests for {@link FreonS3TraceContextInterceptor}. */ +class TestFreonS3TraceContextInterceptor { + private static final String TRACE_ID = "0af7651916cd43dd8448eb211c80319c"; + private static final String SPAN_ID = "b7ad6b7169203331"; + + private final FreonS3TraceContextInterceptor interceptor = new FreonS3TraceContextInterceptor(); + + private static Context.ModifyHttpRequest context(SdkHttpRequest request) { + final Context.ModifyHttpRequest context = mock(Context.ModifyHttpRequest.class); + when(context.httpRequest()).thenReturn(request); + return context; + } + + private static SdkHttpRequest request() { + return SdkHttpRequest.builder().method(SdkHttpMethod.PUT).uri(URI.create("http://localhost:9878/bucket/key")) + .build(); + } + + @Test + void addsTraceContextOfCurrentSpan() { + final Span span = Span.wrap( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + final SdkHttpRequest modified; + try (Scope ignored = span.makeCurrent()) { + modified = interceptor.modifyHttpRequest(context(request()), new ExecutionAttributes()); + } + + assertThat(modified.firstMatchingHeader("traceparent")).hasValue("00-" + TRACE_ID + "-" + SPAN_ID + "-01"); + } + + @Test + void leavesRequestUnchangedWithoutSpan() { + final SdkHttpRequest request = request(); + + assertSame(request, interceptor.modifyHttpRequest(context(request), new ExecutionAttributes())); + } +} diff --git a/hadoop-ozone/integration-test-s3/pom.xml b/hadoop-ozone/integration-test-s3/pom.xml index fb951dfbfa9a..283cada97836 100644 --- a/hadoop-ozone/integration-test-s3/pom.xml +++ b/hadoop-ozone/integration-test-s3/pom.xml @@ -26,16 +26,6 @@ Apache Ozone Integration Tests with S3 Gateway - - com.amazonaws - aws-java-sdk-core - test - - - com.amazonaws - aws-java-sdk-s3 - test - com.google.guava guava diff --git a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/S3ClientFactory.java b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/S3ClientFactory.java index 9ad26f180f6f..3186b97b97cf 100644 --- a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/S3ClientFactory.java +++ b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/S3ClientFactory.java @@ -23,14 +23,6 @@ import static org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTPS_ADDRESS_KEY; import static org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTP_ADDRESS_KEY; -import com.amazonaws.ClientConfiguration; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; import java.net.URI; import org.apache.hadoop.hdds.conf.OzoneConfiguration; import org.apache.hadoop.hdds.server.http.HttpConfig; @@ -61,58 +53,6 @@ public S3ClientFactory(OzoneConfiguration conf) { this.conf = conf; } - /** - * Creates an AmazonS3 client (AWS SDK V1) with path style access enabled. - * - * @return AmazonS3 client - */ - public AmazonS3 createS3Client() { - return createS3Client(true); - } - - /** - * Creates an AmazonS3 client (AWS SDK V1). - * - * @param enablePathStyle whether to enable path style access - * @return AmazonS3 client - */ - public AmazonS3 createS3Client(boolean enablePathStyle) { - final String accessKey = "user"; - final String secretKey = "password"; - final Regions region = Regions.DEFAULT_REGION; - - final String protocol; - final HttpConfig.Policy webPolicy = getHttpPolicy(conf); - String host; - - if (webPolicy.isHttpsEnabled()) { - // TODO: Currently HTTPS is disabled in the test, we can add HTTPS - // integration in the future - protocol = HTTPS_SCHEME; - host = conf.get(OZONE_S3G_HTTPS_ADDRESS_KEY); - } else { - protocol = HTTP_SCHEME; - host = conf.get(OZONE_S3G_HTTP_ADDRESS_KEY); - } - - String endpoint = protocol + "://" + host; - - AWSCredentialsProvider credentials = new AWSStaticCredentialsProvider( - new BasicAWSCredentials(accessKey, secretKey)); - - ClientConfiguration clientConfiguration = new ClientConfiguration(); - LOG.info("S3 Endpoint is {}", endpoint); - - return AmazonS3ClientBuilder.standard() - .withPathStyleAccessEnabled(enablePathStyle) - .withEndpointConfiguration( - new AwsClientBuilder.EndpointConfiguration( - endpoint, region.getName())) - .withClientConfiguration(clientConfiguration) - .withCredentials(credentials) - .build(); - } - /** * Creates an S3Client (AWS SDK V2) with path style access enabled. * diff --git a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/OzoneS3SDKTests.java b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/OzoneS3SDKTests.java index 78e801ef9e0f..30ac8729eaf3 100644 --- a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/OzoneS3SDKTests.java +++ b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/OzoneS3SDKTests.java @@ -23,7 +23,6 @@ import org.apache.hadoop.ozone.om.OMConfigKeys; import org.apache.hadoop.ozone.s3.MultiS3GatewayService; import org.apache.hadoop.ozone.s3.S3GatewayService; -import org.apache.hadoop.ozone.s3.awssdk.v1.AbstractS3SDKV1Tests; import org.apache.hadoop.ozone.s3.awssdk.v2.AbstractS3SDKV2Tests; import org.apache.ozone.test.ClusterForTests; import org.junit.jupiter.api.Nested; @@ -48,14 +47,6 @@ protected MiniOzoneCluster createCluster() throws Exception { .build(); } - @Nested - class V1 extends AbstractS3SDKV1Tests { - @Override - public MiniOzoneCluster cluster() { - return getCluster(); - } - } - @Nested class V2 extends AbstractS3SDKV2Tests { @Override diff --git a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java deleted file mode 100644 index a8dfa914b128..000000000000 --- a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java +++ /dev/null @@ -1,3167 +0,0 @@ -/* - * 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.hadoop.ozone.s3.awssdk.v1; - -import static org.apache.hadoop.ozone.OzoneConsts.MB; -import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.calculateDigest; -import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.createFile; -import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.getOmKeyModificationTime; -import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.getOmPartModificationTime; -import static org.apache.hadoop.ozone.s3.util.S3Consts.CUSTOM_METADATA_HEADER_PREFIX; -import static org.apache.hadoop.ozone.s3.util.S3Utils.stripQuotes; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.amazonaws.AmazonServiceException; -import com.amazonaws.AmazonServiceException.ErrorType; -import com.amazonaws.HttpMethod; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.Headers; -import com.amazonaws.services.s3.model.AbortIncompleteMultipartUpload; -import com.amazonaws.services.s3.model.AbortMultipartUploadRequest; -import com.amazonaws.services.s3.model.AccessControlList; -import com.amazonaws.services.s3.model.Bucket; -import com.amazonaws.services.s3.model.BucketLifecycleConfiguration; -import com.amazonaws.services.s3.model.BucketTaggingConfiguration; -import com.amazonaws.services.s3.model.CanonicalGrantee; -import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest; -import com.amazonaws.services.s3.model.CompleteMultipartUploadResult; -import com.amazonaws.services.s3.model.CopyObjectRequest; -import com.amazonaws.services.s3.model.CopyObjectResult; -import com.amazonaws.services.s3.model.CopyPartRequest; -import com.amazonaws.services.s3.model.CopyPartResult; -import com.amazonaws.services.s3.model.CreateBucketRequest; -import com.amazonaws.services.s3.model.DeleteBucketTaggingConfigurationRequest; -import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; -import com.amazonaws.services.s3.model.GetBucketAccelerateConfigurationRequest; -import com.amazonaws.services.s3.model.GetBucketLifecycleConfigurationRequest; -import com.amazonaws.services.s3.model.GetBucketTaggingConfigurationRequest; -import com.amazonaws.services.s3.model.GetObjectAclRequest; -import com.amazonaws.services.s3.model.GetObjectRequest; -import com.amazonaws.services.s3.model.GetObjectTaggingRequest; -import com.amazonaws.services.s3.model.GetObjectTaggingResult; -import com.amazonaws.services.s3.model.Grantee; -import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest; -import com.amazonaws.services.s3.model.InitiateMultipartUploadResult; -import com.amazonaws.services.s3.model.ListBucketsPaginatedRequest; -import com.amazonaws.services.s3.model.ListBucketsPaginatedResult; -import com.amazonaws.services.s3.model.ListMultipartUploadsRequest; -import com.amazonaws.services.s3.model.ListObjectsRequest; -import com.amazonaws.services.s3.model.ListObjectsV2Request; -import com.amazonaws.services.s3.model.ListObjectsV2Result; -import com.amazonaws.services.s3.model.ListPartsRequest; -import com.amazonaws.services.s3.model.MultipartUpload; -import com.amazonaws.services.s3.model.MultipartUploadListing; -import com.amazonaws.services.s3.model.ObjectListing; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.ObjectTagging; -import com.amazonaws.services.s3.model.Owner; -import com.amazonaws.services.s3.model.PartETag; -import com.amazonaws.services.s3.model.PartListing; -import com.amazonaws.services.s3.model.PartSummary; -import com.amazonaws.services.s3.model.Permission; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.amazonaws.services.s3.model.PutObjectResult; -import com.amazonaws.services.s3.model.S3Object; -import com.amazonaws.services.s3.model.S3ObjectInputStream; -import com.amazonaws.services.s3.model.S3ObjectSummary; -import com.amazonaws.services.s3.model.SetBucketLifecycleConfigurationRequest; -import com.amazonaws.services.s3.model.SetBucketTaggingConfigurationRequest; -import com.amazonaws.services.s3.model.SetObjectAclRequest; -import com.amazonaws.services.s3.model.SetObjectTaggingRequest; -import com.amazonaws.services.s3.model.Tag; -import com.amazonaws.services.s3.model.TagSet; -import com.amazonaws.services.s3.model.UploadPartRequest; -import com.amazonaws.services.s3.model.UploadPartResult; -import com.amazonaws.services.s3.transfer.TransferManager; -import com.amazonaws.services.s3.transfer.TransferManagerBuilder; -import com.amazonaws.services.s3.transfer.Upload; -import com.amazonaws.services.s3.transfer.model.UploadResult; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import javax.xml.bind.DatatypeConverter; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.RandomStringUtils; -import org.apache.hadoop.hdds.client.OzoneQuota; -import org.apache.hadoop.hdds.client.ReplicationConfig; -import org.apache.hadoop.hdds.client.ReplicationFactor; -import org.apache.hadoop.hdds.client.ReplicationType; -import org.apache.hadoop.ozone.MiniOzoneCluster; -import org.apache.hadoop.ozone.client.BucketArgs; -import org.apache.hadoop.ozone.client.ObjectStore; -import org.apache.hadoop.ozone.client.OzoneBucket; -import org.apache.hadoop.ozone.client.OzoneClient; -import org.apache.hadoop.ozone.client.OzoneKeyDetails; -import org.apache.hadoop.ozone.client.OzoneVolume; -import org.apache.hadoop.ozone.client.io.OzoneOutputStream; -import org.apache.hadoop.ozone.om.OMMetadataManager; -import org.apache.hadoop.ozone.om.OzoneManager; -import org.apache.hadoop.ozone.om.helpers.BucketLayout; -import org.apache.hadoop.ozone.om.helpers.OmBucketInfo; -import org.apache.hadoop.ozone.om.helpers.OmMultipartKeyInfo; -import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol; -import org.apache.hadoop.ozone.om.service.KeyLifecycleService; -import org.apache.hadoop.ozone.s3.S3ClientFactory; -import org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils; -import org.apache.hadoop.ozone.s3.endpoint.S3Owner; -import org.apache.hadoop.ozone.s3.exception.S3ErrorTable; -import org.apache.hadoop.ozone.s3.util.S3Consts; -import org.apache.hadoop.security.UserGroupInformation; -import org.apache.ozone.test.GenericTestUtils; -import org.apache.ozone.test.NonHATests; -import org.apache.ozone.test.OzoneTestBase; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.function.Executable; -import org.junit.jupiter.api.io.TempDir; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; - -/** - * This is an abstract class to test the AWS Java S3 SDK operations. - * This class should be extended for OM standalone and OM HA (Ratis) cluster setup. - * - * The test scenarios are adapted from - * - https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/java/example_code/s3/ - * - https://github.com/ceph/s3-tests - * - */ -@TestMethodOrder(MethodOrderer.MethodName.class) -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public abstract class AbstractS3SDKV1Tests extends OzoneTestBase implements NonHATests.TestCase { - - // server-side limitation - private static final int MAX_UPLOADS_LIMIT = 1000; - private static final String READ_CONSISTENCY_HEADER = - "x-ozone-read-consistency"; - private static final String LOCAL_LEASE_LOG_LIMIT_HEADER = - "x-ozone-local-lease-log-limit"; - - /** - * There are still some unsupported S3 operations. - * Current unsupported S3 operations (non-exhaustive): - * - Cross Region Replication (CrossRegionReplication.java) - * - Versioned enabled buckets - * - DeleteObjectVersionEnabledBucket.java - * - DeleteMultipleObjectsVersionEnabledBucket.java - * - ListKeysVersioningEnabledBucket.java - * - Website configurations - * - WebsiteConfiguration.java - * - SetWebsiteConfiguration.java - * - GetWebsiteConfiguration.java - * - DeleteWebsiteConfiguration.java - * - S3 Event Notifications - * - EnableNotificationOnABucket.java - * - Object tags - * - GetObjectTags.java - * - GetObjectTags2.java - * - Bucket policy - * - SetBucketPolicy.java - * - GetBucketPolicy.java - * - DeleteBucketPolicy.java - * - Bucket lifecycle configuration - * - LifecycleConfiguration.java - * - Canned Bucket ACL - * - CreateBucketWithACL.java - * - Object ACL - * - SetAcl.java - * - ModifyACLExistingObject.java - * - GetAcl.java - * - S3 Encryption - * - S3Encrypt.java - * - S3EncryptV2.java - * - Client-side encryption - * - S3ClientSideEncryptionAsymmetricMasterKey.java - * - S3ClientSideEncryptionSymMasterKey.java - * - Server-side encryption - * - SpecifyServerSideEncryption.ajva - * - ServerSideEncryptionCopyObjectUsingHLWithSSEC.java - * - ServerSideEncryptionUsingClientSideEncryptionKey.java - * - Dual stack endpoints - * - DualStackEndpoints.java - * - Transfer acceleration - * - TransferAcceleration.java - * - Temp credentials - * - MakingRequestsWithFederatedTempCredentials.java - * - MakingRequestsWithIAMTempCredentials.java - * - Object archival - * - RestoreArchivedObject - * - KMS key - * - UploadObjectKMSKey.java - */ - - private MiniOzoneCluster cluster; - private AmazonS3 s3Client; - - @BeforeAll - void createClient() { - cluster = cluster(); - s3Client = new S3ClientFactory(cluster.getConf()) - .createS3Client(); - } - - @Test - public void testCreateBucket() { - final String bucketName = getBucketName(); - - Bucket b = s3Client.createBucket(bucketName); - - assertEquals(bucketName, b.getName()); - assertTrue(s3Client.doesBucketExist(bucketName)); - assertTrue(s3Client.doesBucketExistV2(bucketName)); - assertTrue(isBucketEmpty(b)); - } - - /** - * s3-tests: test_bucket_create_exists. - */ - @Test - public void testCreateBucketAlreadyOwnedByYou() { - final String bucketName = getBucketName("owned-by-you"); - s3Client.createBucket(bucketName); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.createBucket(bucketName)); - assertEquals(409, ase.getStatusCode()); - assertEquals(S3ErrorTable.BUCKET_ALREADY_OWNED_BY_YOU.getCode(), ase.getErrorCode()); - assertEquals("application/xml", ase.getHttpHeaders().get("Content-Type")); - } - - @Test - public void testCreateBucketAlreadyExistsDifferentOwner() throws IOException { - final String bucketName = getBucketName("other-owner"); - final String otherOwner = "other-s3-owner"; - try (OzoneClient ozoneClient = cluster.newClient()) { - ozoneClient.getObjectStore().getS3Volume().createBucket(bucketName, - BucketArgs.newBuilder().setOwner(otherOwner).build()); - } - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.createBucket(bucketName)); - assertEquals(409, ase.getStatusCode()); - assertEquals(S3ErrorTable.BUCKET_ALREADY_EXISTS.getCode(), ase.getErrorCode()); - } - - @Test - public void testBucketACLOperations() { - // TODO HDDS-11738: Uncomment assertions when bucket S3 ACL logic has been fixed - final String bucketName = getBucketName(); - - AccessControlList aclList = new AccessControlList(); - Owner owner = new Owner("owner", "owner"); - aclList.withOwner(owner); - Grantee grantee = new CanonicalGrantee("testGrantee"); - aclList.grantPermission(grantee, Permission.Read); - - - CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName) - .withAccessControlList(aclList); - - s3Client.createBucket(createBucketRequest); - - //assertEquals(aclList, s3Client.getBucketAcl(bucketName)); - - aclList.grantPermission(grantee, Permission.Write); - s3Client.setBucketAcl(bucketName, aclList); - - //assertEquals(aclList, s3Client.getBucketAcl(bucketName)); - } - - /** - * Integration tests for ListBuckets (GET / ListAllMyBuckets). - */ - @Nested - class ListBucketsTests { - - @Test - public void testListBuckets() throws IOException { - List bucketNames = new ArrayList<>(); - for (int i = 0; i <= 5; i++) { - String bucketName = getBucketName(String.valueOf(i)); - s3Client.createBucket(bucketName); - bucketNames.add(bucketName); - } - - List bucketList = s3Client.listBuckets(); - List listBucketNames = bucketList.stream() - .map(Bucket::getName).collect(Collectors.toList()); - - assertThat(listBucketNames).containsAll(bucketNames); - - UserGroupInformation ugi = UserGroupInformation.getCurrentUser(); - String expectOwner = ugi.getShortUserName(); - - Owner s3AccountOwner = s3Client.getS3AccountOwner(); - - assertThat(s3AccountOwner.getDisplayName()).isEqualTo(expectOwner); - assertThat(s3AccountOwner.getId()).isEqualTo(S3Owner.DEFAULT_S3OWNER_ID); - } - - /** - * Verifies {@code maxBuckets=1} returns one bucket per page and a continuation token - * when more buckets exist. - */ - @Test - public void testListBucketsPaginatedMaxBucketsOne() { - final String bucketA = uniqueObjectName("bucket-a"); - final String bucketB = uniqueObjectName("bucket-b"); - s3Client.createBucket(bucketA); - s3Client.createBucket(bucketB); - try { - List found = S3SDKTestUtils.collectBucketsOnePerPage((token, max) -> { - ListBucketsPaginatedRequest request = new ListBucketsPaginatedRequest() - .withMaxBuckets(max); - if (token != null) { - request.withContinuationToken(token); - } - - ListBucketsPaginatedResult page = s3Client.listBuckets(request); - return new S3SDKTestUtils.BucketListPage( - page.getBuckets().stream().map(Bucket::getName).collect(Collectors.toList()), - page.getContinuationToken()); - }); - List foundTestBuckets = S3SDKTestUtils.filterToExpectedBuckets( - found, bucketA, bucketB); - assertThat(foundTestBuckets).containsExactlyInAnyOrder(bucketA, bucketB); - } finally { - s3Client.deleteBucket(bucketA); - s3Client.deleteBucket(bucketB); - } - } - - /** - * Verifies pagination: listing buckets page-by-page using {@code maxBuckets} - * and the returned continuation token, until all buckets are retrieved. - */ - @Test - public void testListBucketsPaginationReturnsAllBuckets() { - final int totalBuckets = 5; - final int pageSize = 2; - List created = new ArrayList<>(); - - for (int i = 0; i < totalBuckets; i++) { - String name = uniqueObjectName("paginated-" + i); - s3Client.createBucket(name); - created.add(name); - } - - try { - List retrieved = new ArrayList<>(); - String continuationToken = null; - - do { - ListBucketsPaginatedRequest request = new ListBucketsPaginatedRequest() - .withMaxBuckets(pageSize); - if (continuationToken != null) { - request.withContinuationToken(continuationToken); - } - - ListBucketsPaginatedResult response = s3Client.listBuckets(request); - - response.getBuckets().stream() - .map(Bucket::getName) - .filter(created::contains) - .forEach(retrieved::add); - - continuationToken = response.getContinuationToken(); - } while (continuationToken != null); - - assertThat(retrieved).containsExactlyInAnyOrderElementsOf(created); - } finally { - for (String name : created) { - s3Client.deleteBucket(name); - } - } - } - - /** - * verifies that Page 1 uses maxBuckets=1; - * later pages send only continuationToken (no maxBuckets). - */ - @Test - public void testListBucketsContinuationTokenWithoutMaxBuckets() { - List created = new ArrayList<>(); - for (int i = 0; i < 3; i++) { - String name = uniqueObjectName("token-only-" + i); - s3Client.createBucket(name); - created.add(name); - } - - try { - List retrieved = new ArrayList<>(); - String continuationToken = null; - boolean firstPage = true; - - do { - ListBucketsPaginatedRequest request = new ListBucketsPaginatedRequest(); - if (firstPage) { - request.withMaxBuckets(1); - firstPage = false; - } else { - request.withContinuationToken(continuationToken); - } - - ListBucketsPaginatedResult response = s3Client.listBuckets(request); - if (continuationToken == null) { - assertEquals(1, response.getBuckets().size()); - assertNotNull(response.getContinuationToken()); - } else { - assertFalse(response.getBuckets().isEmpty()); - } - - response.getBuckets().stream() - .map(Bucket::getName) - .filter(created::contains) - .forEach(retrieved::add); - - continuationToken = response.getContinuationToken(); - } while (continuationToken != null); - - assertThat(retrieved).containsExactlyInAnyOrderElementsOf(created); - } finally { - for (String name : created) { - s3Client.deleteBucket(name); - } - } - } - } - - @Test - public void testDeleteBucket() { - final String bucketName = getBucketName(); - - s3Client.createBucket(bucketName); - - s3Client.deleteBucket(bucketName); - - assertFalse(s3Client.doesBucketExist(bucketName)); - assertFalse(s3Client.doesBucketExistV2(bucketName)); - } - - @Test - public void testDeleteBucketNotExist() { - final String bucketName = getBucketName(); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.deleteBucket(bucketName)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(404, ase.getStatusCode()); - assertEquals("NoSuchBucket", ase.getErrorCode()); - } - - @Test - public void testDeleteBucketNonEmptyWithKeys() { - final String bucketName = getBucketName(); - s3Client.createBucket(bucketName); - - // Upload some objects to the bucket - for (int i = 1; i <= 10; i++) { - s3Client.putObject(bucketName, "key-" + i, RandomStringUtils.secure().nextAlphanumeric(1024)); - } - - // Bucket deletion should fail if there are still keys in the bucket - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.deleteBucket(bucketName) - ); - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(409, ase.getStatusCode()); - assertEquals("BucketNotEmpty", ase.getErrorCode()); - - // Delete all the keys - ObjectListing objectListing = s3Client.listObjects(bucketName); - while (true) { - for (S3ObjectSummary summary : objectListing.getObjectSummaries()) { - s3Client.deleteObject(bucketName, summary.getKey()); - } - - // more object_listing to retrieve? - if (objectListing.isTruncated()) { - objectListing = s3Client.listNextBatchOfObjects(objectListing); - } else { - break; - } - } - } - - @Test - public void testDeleteBucketNonEmptyWithIncompleteMultipartUpload(@TempDir Path tempDir) throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - - File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); - - createFile(multipartUploadFile, (int) (5 * MB)); - - // Create an incomplete multipart upload by initiating multipart upload, - // uploading some parts, but not actually completing it. - String uploadId = initiateMultipartUpload(bucketName, keyName, null, null, null); - - uploadParts(bucketName, keyName, uploadId, multipartUploadFile, 1 * MB); - - // Bucket deletion should fail if there are still keys in the bucket - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.deleteBucket(bucketName) - ); - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(409, ase.getStatusCode()); - assertEquals("BucketNotEmpty", ase.getErrorCode()); - - // After the multipart upload is aborted, the bucket deletion should succeed - abortMultipartUpload(bucketName, keyName, uploadId); - - s3Client.deleteBucket(bucketName); - - assertFalse(s3Client.doesBucketExistV2(bucketName)); - } - - @Test - public void testPutObject() { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - - PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName, is, new ObjectMetadata()); - assertEquals("37b51d194a7513e45b56f6524f2d51f2", putObjectResult.getETag()); - } - - @ParameterizedTest - @ValueSource(strings = {"follower-stale", "follower-linearizable", - "leader-only"}) - public void testGetObjectWithReadConsistencyHeader(String readConsistency) - throws IOException { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - s3Client.putObject(bucketName, keyName, content); - - GetObjectRequest request = new GetObjectRequest(bucketName, keyName); - request.putCustomRequestHeader(READ_CONSISTENCY_HEADER, - readConsistency); - if ("follower-stale".equals(readConsistency)) { - request.putCustomRequestHeader(LOCAL_LEASE_LOG_LIMIT_HEADER, "10"); - } - - try (S3Object object = s3Client.getObject(request); - S3ObjectInputStream objectContent = object.getObjectContent()) { - assertEquals(content, IOUtils.toString(objectContent, - StandardCharsets.UTF_8)); - } - } - - @Test - public void testGetObjectWithInvalidReadConsistencyHeader() { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - s3Client.putObject(bucketName, keyName, "bar"); - - GetObjectRequest request = new GetObjectRequest(bucketName, keyName); - request.putCustomRequestHeader(READ_CONSISTENCY_HEADER, - "invalid"); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.getObject(request)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(400, ase.getStatusCode()); - assertEquals("InvalidArgument", ase.getErrorCode()); - } - - @Test - public void testPutObjectWithEmptyContentType() { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentType(""); - InputStream inputStream = new ByteArrayInputStream( - content.getBytes(StandardCharsets.UTF_8)); - - PutObjectResult putObjectResult = s3Client.putObject( - bucketName, keyName, inputStream, metadata); - assertEquals("37b51d194a7513e45b56f6524f2d51f2", - putObjectResult.getETag()); - } - - @Test - public void testGetObjectWithMalformedReadConsistencyHeader() { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - s3Client.putObject(bucketName, keyName, "bar"); - - GetObjectRequest request = new GetObjectRequest(bucketName, keyName); - request.putCustomRequestHeader(READ_CONSISTENCY_HEADER, - "follower-stale;logLimit=10"); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.getObject(request)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(400, ase.getStatusCode()); - assertEquals("InvalidArgument", ase.getErrorCode()); - } - - @Test - public void testPutObjectIfNoneMatch() { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - PutObjectRequest request = new PutObjectRequest( - bucketName, keyName, is, new ObjectMetadata()).ifNoneMatch("*"); - - PutObjectResult putObjectResult = s3Client.putObject(request); - assertEquals("37b51d194a7513e45b56f6524f2d51f2", putObjectResult.getETag()); - } - - @Test - public void testPutObjectIfNoneMatchFail() { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - s3Client.putObject(bucketName, keyName, is, new ObjectMetadata()); - - InputStream is2 = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - PutObjectRequest request = new PutObjectRequest( - bucketName, keyName, is2, new ObjectMetadata()).ifNoneMatch("*"); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.putObject(request)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(412, ase.getStatusCode()); - assertEquals("PreconditionFailed", ase.getErrorCode()); - } - - @Test - public void testPutObjectIfMatch() { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName, is, new ObjectMetadata()); - String etag = putObjectResult.getETag(); - - InputStream is2 = new ByteArrayInputStream("bar2".getBytes(StandardCharsets.UTF_8)); - PutObjectRequest request = new PutObjectRequest( - bucketName, keyName, is2, new ObjectMetadata()).ifMatch(etag); - - PutObjectResult putObjectResult2 = s3Client.putObject(request); - assertNotNull(putObjectResult2.getETag()); - assertNotEquals(etag, putObjectResult2.getETag()); - - ObjectMetadata updatedObjectMetadata = s3Client.getObjectMetadata(bucketName, keyName); - assertEquals(putObjectResult2.getETag(), updatedObjectMetadata.getETag()); - } - - @Test - public void testPutObjectIfMatchFail() { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - PutObjectResult initialResult = - s3Client.putObject(bucketName, keyName, is, new ObjectMetadata()); - - InputStream is2 = new ByteArrayInputStream("bar2".getBytes(StandardCharsets.UTF_8)); - PutObjectRequest request = new PutObjectRequest( - bucketName, keyName, is2, new ObjectMetadata()).ifMatch("wrong-etag"); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.putObject(request)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(412, ase.getStatusCode()); - assertEquals("PreconditionFailed", ase.getErrorCode()); - - ObjectMetadata existingObjectMetadata = s3Client.getObjectMetadata(bucketName, keyName); - assertEquals(initialResult.getETag(), existingObjectMetadata.getETag()); - } - - @ParameterizedTest - @ValueSource(strings = {"*", "\"some-etag\""}) - public void testPutObjectIfMatchMissingKeyFail(String ifMatch) { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - - InputStream is = new ByteArrayInputStream("bar2".getBytes( - StandardCharsets.UTF_8)); - PutObjectRequest request = new PutObjectRequest( - bucketName, keyName, is, new ObjectMetadata()).ifMatch(ifMatch); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.putObject(request)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(404, ase.getStatusCode()); - assertEquals("NoSuchKey", ase.getErrorCode()); - - AmazonServiceException missingKey = assertThrows(AmazonServiceException.class, - () -> s3Client.getObject(bucketName, keyName)); - assertEquals(ErrorType.Client, missingKey.getErrorType()); - assertEquals(404, missingKey.getStatusCode()); - assertEquals("NoSuchKey", missingKey.getErrorCode()); - } - - @Test - public void testDeleteObjectIfMatch() throws IOException { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName, is, new ObjectMetadata()); - - int responseCode = deleteObjectWithIfMatch(bucketName, keyName, putObjectResult.getETag()); - - assertEquals(HttpURLConnection.HTTP_NO_CONTENT, responseCode); - assertFalse(s3Client.doesObjectExist(bucketName, keyName)); - } - - @Test - public void testDeleteObjectIfMatchFail() throws IOException { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName, is, new ObjectMetadata()); - - int responseCode = deleteObjectWithIfMatch(bucketName, keyName, "wrong-etag"); - - assertEquals(HttpURLConnection.HTTP_PRECON_FAILED, responseCode); - ObjectMetadata existingObjectMetadata = s3Client.getObjectMetadata(bucketName, keyName); - assertEquals(putObjectResult.getETag(), existingObjectMetadata.getETag()); - } - - @Test - public void testDeleteObjectIfMatchWildcard() throws IOException { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - s3Client.putObject(bucketName, keyName, is, new ObjectMetadata()); - - int responseCode = deleteObjectWithIfMatch(bucketName, keyName, "*"); - - assertEquals(HttpURLConnection.HTTP_NO_CONTENT, responseCode); - assertFalse(s3Client.doesObjectExist(bucketName, keyName)); - } - - @Test - public void testDeleteObjectIfMatchWildcardMissingKeyFail() throws IOException { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - - int responseCode = deleteObjectWithIfMatch(bucketName, keyName, "*"); - - assertEquals(HttpURLConnection.HTTP_PRECON_FAILED, responseCode); - assertFalse(s3Client.doesObjectExist(bucketName, keyName)); - } - - private int deleteObjectWithIfMatch(String bucketName, String keyName, String ifMatch) throws IOException { - GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, keyName) - .withMethod(HttpMethod.DELETE) - .withExpiration(Date.from(Instant.now().plusMillis(1000 * 60 * 60))); - request.putCustomRequestHeader(Headers.GET_OBJECT_IF_MATCH, ifMatch); - URL presignedUrl = s3Client.generatePresignedUrl(request); - Map> headers = Collections.singletonMap(Headers.GET_OBJECT_IF_MATCH, - Collections.singletonList(ifMatch)); - - HttpURLConnection connection = null; - try { - connection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "DELETE", headers, null); - return connection.getResponseCode(); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - } - - @Test - public void testCopyObject() { - final String sourceBucketName = getBucketName("source"); - final String destBucketName = getBucketName("dest"); - final String sourceKey = getKeyName("source"); - final String destKey = getKeyName("dest"); - final String content = "bar"; - s3Client.createBucket(sourceBucketName); - s3Client.createBucket(destBucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - PutObjectResult putResult = s3Client.putObject(sourceBucketName, sourceKey, is, new ObjectMetadata()); - assertEquals("37b51d194a7513e45b56f6524f2d51f2", putResult.getETag()); - - CopyObjectResult copyResult = s3Client.copyObject(sourceBucketName, sourceKey, destBucketName, destKey); - assertEquals("37b51d194a7513e45b56f6524f2d51f2", copyResult.getETag()); - } - - @Test - public void testCopyObjectLastModifiedMatchesOmDb() throws Exception { - final String sourceBucketName = getBucketName(); - final String destBucketName = getBucketName(); - final String sourceKey = getKeyName(); - final String destKey = getKeyName(); - final String content = "copy-last-modified-test-content"; - s3Client.createBucket(sourceBucketName); - s3Client.createBucket(destBucketName); - - final InputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - s3Client.putObject(sourceBucketName, sourceKey, inputStream, new ObjectMetadata()); - - final long sourceModificationTime = getOmKeyModificationTime(cluster, sourceBucketName, sourceKey); - - Thread.sleep(100); - - final CopyObjectResult copyResult = s3Client.copyObject(sourceBucketName, sourceKey, destBucketName, destKey); - - assertNotNull(copyResult.getLastModifiedDate()); - final long responseModificationTime = copyResult.getLastModifiedDate().getTime(); - final long destModificationTime = getOmKeyModificationTime(cluster, destBucketName, destKey); - - assertEquals(destModificationTime, responseModificationTime); - assertNotEquals(sourceModificationTime, responseModificationTime); - } - - @Test - public void testUploadPartCopyLastModifiedMatchesOmDb() throws Exception { - final String bucketName = getBucketName(); - final String sourceKey = getKeyName(); - final String destKey = getKeyName(); - final String content = "copy-last-modified-test-content"; - s3Client.createBucket(bucketName); - - final InputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - s3Client.putObject(bucketName, sourceKey, inputStream, new ObjectMetadata()); - - final long sourceModificationTime = getOmKeyModificationTime(cluster, bucketName, sourceKey); - - Thread.sleep(100); - - final InitiateMultipartUploadResult initResponse = s3Client.initiateMultipartUpload( - new InitiateMultipartUploadRequest(bucketName, destKey)); - final String uploadId = initResponse.getUploadId(); - - final CopyPartResult copyPartResult = s3Client.copyPart( - new CopyPartRequest() - .withSourceBucketName(bucketName) - .withSourceKey(sourceKey) - .withDestinationBucketName(bucketName) - .withDestinationKey(destKey) - .withUploadId(uploadId) - .withPartNumber(1)); - - assertNotNull(copyPartResult.getLastModifiedDate()); - final long responseModificationTime = copyPartResult.getLastModifiedDate().getTime(); - final long destPartModificationTime = getOmPartModificationTime(cluster, bucketName, destKey, uploadId, 1); - - assertEquals(destPartModificationTime, responseModificationTime); - assertNotEquals(sourceModificationTime, responseModificationTime); - } - - @Test - public void testCopyObjectWithSourceIfMatch() { - final String sourceBucketName = getBucketName("source"); - final String destBucketName = getBucketName("dest"); - final String sourceKey = getKeyName("source"); - final String destKey = getKeyName("dest"); - final String content = "bar"; - s3Client.createBucket(sourceBucketName); - s3Client.createBucket(destBucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - PutObjectResult putResult = s3Client.putObject(sourceBucketName, sourceKey, is, new ObjectMetadata()); - String sourceETag = putResult.getETag(); - - CopyObjectRequest copyRequest = new CopyObjectRequest(sourceBucketName, sourceKey, destBucketName, destKey) - .withMatchingETagConstraint(sourceETag); - CopyObjectResult copyResult = s3Client.copyObject(copyRequest); - assertEquals(sourceETag, copyResult.getETag()); - } - - @Test - public void testCopyObjectWithSourceIfMatchFail() { - final String sourceBucketName = getBucketName("source"); - final String destBucketName = getBucketName("dest"); - final String sourceKey = getKeyName("source"); - final String destKey = getKeyName("dest"); - final String content = "bar"; - s3Client.createBucket(sourceBucketName); - s3Client.createBucket(destBucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - s3Client.putObject(sourceBucketName, sourceKey, is, new ObjectMetadata()); - - CopyObjectRequest copyRequest = new CopyObjectRequest(sourceBucketName, sourceKey, destBucketName, destKey) - .withMatchingETagConstraint("wrong-etag"); - - CopyObjectResult copyResult = s3Client.copyObject(copyRequest); - assertNull(copyResult); - } - - @Test - public void testCopyObjectWithSourceIfNoneMatch() { - final String sourceBucketName = getBucketName("source"); - final String destBucketName = getBucketName("dest"); - final String sourceKey = getKeyName("source"); - final String destKey = getKeyName("dest"); - final String content = "bar"; - s3Client.createBucket(sourceBucketName); - s3Client.createBucket(destBucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - PutObjectResult putResult = s3Client.putObject(sourceBucketName, sourceKey, is, new ObjectMetadata()); - String sourceETag = putResult.getETag(); - - CopyObjectRequest copyRequest = new CopyObjectRequest(sourceBucketName, sourceKey, destBucketName, destKey) - .withNonmatchingETagConstraint("different-etag"); - CopyObjectResult copyResult = s3Client.copyObject(copyRequest); - assertEquals(sourceETag, copyResult.getETag()); - } - - @Test - public void testCopyObjectWithSourceIfNoneMatchFail() { - final String sourceBucketName = getBucketName("source"); - final String destBucketName = getBucketName("dest"); - final String sourceKey = getKeyName("source"); - final String destKey = getKeyName("dest"); - final String content = "bar"; - s3Client.createBucket(sourceBucketName); - s3Client.createBucket(destBucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - PutObjectResult putResult = s3Client.putObject(sourceBucketName, sourceKey, is, new ObjectMetadata()); - String sourceETag = putResult.getETag(); - - CopyObjectRequest copyRequest = new CopyObjectRequest(sourceBucketName, sourceKey, destBucketName, destKey) - .withNonmatchingETagConstraint(sourceETag); - - CopyObjectResult copyResult = s3Client.copyObject(copyRequest); - assertNull(copyResult); - } - - @Test - public void testPutObjectWithMD5Header() throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - - byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); - byte[] md5Bytes = calculateDigest(new ByteArrayInputStream(contentBytes), 0, contentBytes.length); - String md5Base64 = Base64.getEncoder().encodeToString(md5Bytes); - - InputStream is = new ByteArrayInputStream(contentBytes); - ObjectMetadata objectMetadata = new ObjectMetadata(); - objectMetadata.setContentMD5(md5Base64); - objectMetadata.setContentLength(contentBytes.length); - - PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName, is, objectMetadata); - assertEquals("37b51d194a7513e45b56f6524f2d51f2", putObjectResult.getETag()); - - S3Object object = s3Client.getObject(bucketName, keyName); - assertEquals(content.length(), object.getObjectMetadata().getContentLength()); - assertEquals("37b51d194a7513e45b56f6524f2d51f2", object.getObjectMetadata().getETag()); - } - - static Stream wrongContentMD5Provider() throws Exception { - byte[] wrongContentBytes = "wrong".getBytes(StandardCharsets.UTF_8); - byte[] wrongMd5Bytes = MessageDigest.getInstance("MD5").digest(wrongContentBytes); - String wrongMd5Base64 = Base64.getEncoder().encodeToString(wrongMd5Bytes); - - return Stream.of( - Arguments.of(wrongMd5Base64, S3ErrorTable.BAD_DIGEST.getCode()), - Arguments.of("invalid-base64", S3ErrorTable.INVALID_DIGEST.getCode()) - ); - } - - @ParameterizedTest - @MethodSource("wrongContentMD5Provider") - public void testPutObjectWithWrongMD5Header(String wrongMd5Base64, String expectedErrorCode) { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - - byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); - InputStream is = new ByteArrayInputStream(contentBytes); - ObjectMetadata objectMetadata = new ObjectMetadata(); - objectMetadata.setContentMD5(wrongMd5Base64); - objectMetadata.setContentLength(contentBytes.length); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.putObject(bucketName, keyName, is, objectMetadata)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(400, ase.getStatusCode()); - assertEquals(expectedErrorCode, ase.getErrorCode()); - - // Verify the object was not uploaded - assertFalse(s3Client.doesObjectExist(bucketName, keyName)); - } - - @Test - public void testMultipartUploadWithMD5Header() throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - - // Initiate multipart upload - InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(bucketName, keyName); - InitiateMultipartUploadResult initResponse = s3Client.initiateMultipartUpload(initRequest); - String uploadId = initResponse.getUploadId(); - - // Prepare part data - String part1Content = "part1data"; - byte[] part1Bytes = part1Content.getBytes(StandardCharsets.UTF_8); - byte[] part1Md5Bytes = calculateDigest(new ByteArrayInputStream(part1Bytes), 0, part1Bytes.length); - String part1Md5Base64 = Base64.getEncoder().encodeToString(part1Md5Bytes); - - // Upload part 1 with MD5 - InputStream part1InputStream = new ByteArrayInputStream(part1Bytes); - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentMD5(part1Md5Base64); - metadata.setContentLength(part1Bytes.length); - - UploadPartRequest uploadRequest = new UploadPartRequest() - .withBucketName(bucketName) - .withKey(keyName) - .withUploadId(uploadId) - .withPartNumber(1) - .withInputStream(part1InputStream) - .withPartSize(part1Bytes.length) - .withObjectMetadata(metadata); - - UploadPartResult uploadResult = s3Client.uploadPart(uploadRequest); - - // Verify ETag - String expectedETag = DatatypeConverter.printHexBinary(part1Md5Bytes).toLowerCase(); - assertEquals(expectedETag, uploadResult.getPartETag().getETag()); - - // Complete multipart upload - List partETags = new ArrayList<>(); - partETags.add(uploadResult.getPartETag()); - - CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest( - bucketName, keyName, uploadId, partETags); - s3Client.completeMultipartUpload(completeRequest); - - // Verify object was uploaded - S3Object object = s3Client.getObject(bucketName, keyName); - try (S3ObjectInputStream s3is = object.getObjectContent(); - ByteArrayOutputStream bos = new ByteArrayOutputStream()) { - IOUtils.copy(s3is, bos); - assertEquals(part1Content, bos.toString("UTF-8")); - } - } - - @ParameterizedTest - @MethodSource("wrongContentMD5Provider") - public void testMultipartUploadPartWithWrongMD5Header(String wrongMd5Base64, String expectedErrorCode) { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - - // Initiate multipart upload - InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(bucketName, keyName); - InitiateMultipartUploadResult initResponse = s3Client.initiateMultipartUpload(initRequest); - String uploadId = initResponse.getUploadId(); - - // Prepare part data with wrong MD5 - String partContent = "partdata"; - byte[] partBytes = partContent.getBytes(StandardCharsets.UTF_8); - - // Upload part with wrong/invalid MD5 should fail - InputStream partInputStream = new ByteArrayInputStream(partBytes); - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentMD5(wrongMd5Base64); - metadata.setContentLength(partBytes.length); - - UploadPartRequest uploadRequest = new UploadPartRequest() - .withBucketName(bucketName) - .withKey(keyName) - .withUploadId(uploadId) - .withPartNumber(1) - .withInputStream(partInputStream) - .withPartSize(partBytes.length) - .withObjectMetadata(metadata); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.uploadPart(uploadRequest)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(400, ase.getStatusCode()); - assertEquals(expectedErrorCode, ase.getErrorCode()); - - // Abort the multipart upload - AbortMultipartUploadRequest abortRequest = new AbortMultipartUploadRequest(bucketName, keyName, uploadId); - s3Client.abortMultipartUpload(abortRequest); - - // Verify object was not created - assertFalse(s3Client.doesObjectExist(bucketName, keyName)); - } - - @Test - public void testCompleteMultipartUploadIfNoneMatch() throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - - // Initiate and complete multipart upload with If-None-Match: * - InitiateMultipartUploadRequest initRequest = - new InitiateMultipartUploadRequest(bucketName, keyName); - InitiateMultipartUploadResult initResponse = s3Client.initiateMultipartUpload(initRequest); - String uploadId = initResponse.getUploadId(); - - // Upload a part - String partContent = "part1data"; - byte[] partBytes = partContent.getBytes(StandardCharsets.UTF_8); - UploadPartRequest uploadRequest = new UploadPartRequest() - .withBucketName(bucketName) - .withKey(keyName) - .withUploadId(uploadId) - .withPartNumber(1) - .withInputStream(new ByteArrayInputStream(partBytes)) - .withPartSize(partBytes.length); - UploadPartResult uploadResult = s3Client.uploadPart(uploadRequest); - - // Complete with If-None-Match: * (key doesn't exist, should succeed) - List partETags = Collections.singletonList(uploadResult.getPartETag()); - CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest( - bucketName, keyName, uploadId, partETags); - completeRequest.putCustomRequestHeader("If-None-Match", "*"); - - CompleteMultipartUploadResult result = s3Client.completeMultipartUpload(completeRequest); - assertNotNull(result.getETag()); - - // Verify object was created - assertTrue(s3Client.doesObjectExist(bucketName, keyName)); - } - - @Test - public void testCompleteMultipartUploadIfNoneMatchFail() throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - - // First, create an existing key - s3Client.putObject(bucketName, keyName, - new ByteArrayInputStream("existing".getBytes(StandardCharsets.UTF_8)), - new ObjectMetadata()); - - // Initiate multipart upload for same key - InitiateMultipartUploadRequest initRequest = - new InitiateMultipartUploadRequest(bucketName, keyName); - InitiateMultipartUploadResult initResponse = s3Client.initiateMultipartUpload(initRequest); - String uploadId = initResponse.getUploadId(); - - // Upload a part - String partContent = "part1data"; - byte[] partBytes = partContent.getBytes(StandardCharsets.UTF_8); - UploadPartRequest uploadRequest = new UploadPartRequest() - .withBucketName(bucketName) - .withKey(keyName) - .withUploadId(uploadId) - .withPartNumber(1) - .withInputStream(new ByteArrayInputStream(partBytes)) - .withPartSize(partBytes.length); - UploadPartResult uploadResult = s3Client.uploadPart(uploadRequest); - - // Complete with If-None-Match: * (key exists, should fail) - List partETags = Collections.singletonList(uploadResult.getPartETag()); - CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest( - bucketName, keyName, uploadId, partETags); - completeRequest.putCustomRequestHeader("If-None-Match", "*"); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.completeMultipartUpload(completeRequest)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(412, ase.getStatusCode()); - assertEquals("PreconditionFailed", ase.getErrorCode()); - } - - @Test - public void testCompleteMultipartUploadIfMatch() throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - - // First, create an existing key and get its ETag - PutObjectResult existingResult = s3Client.putObject(bucketName, keyName, - new ByteArrayInputStream("existing".getBytes(StandardCharsets.UTF_8)), - new ObjectMetadata()); - String existingETag = existingResult.getETag(); - - // Initiate multipart upload for same key - InitiateMultipartUploadRequest initRequest = - new InitiateMultipartUploadRequest(bucketName, keyName); - InitiateMultipartUploadResult initResponse = s3Client.initiateMultipartUpload(initRequest); - String uploadId = initResponse.getUploadId(); - - // Upload a part - String partContent = "newcontent"; - byte[] partBytes = partContent.getBytes(StandardCharsets.UTF_8); - UploadPartRequest uploadRequest = new UploadPartRequest() - .withBucketName(bucketName) - .withKey(keyName) - .withUploadId(uploadId) - .withPartNumber(1) - .withInputStream(new ByteArrayInputStream(partBytes)) - .withPartSize(partBytes.length); - UploadPartResult uploadResult = s3Client.uploadPart(uploadRequest); - - // Complete with If-Match: (should succeed) - List partETags = Collections.singletonList(uploadResult.getPartETag()); - CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest( - bucketName, keyName, uploadId, partETags); - completeRequest.putCustomRequestHeader("If-Match", "\"" + existingETag + "\""); - - CompleteMultipartUploadResult result = s3Client.completeMultipartUpload(completeRequest); - assertNotNull(result.getETag()); - assertNotEquals(existingETag, stripQuotes(result.getETag())); - } - - @Test - public void testCompleteMultipartUploadIfMatchFail() throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - - // First, create an existing key - s3Client.putObject(bucketName, keyName, - new ByteArrayInputStream("existing".getBytes(StandardCharsets.UTF_8)), - new ObjectMetadata()); - - // Initiate multipart upload for same key - InitiateMultipartUploadRequest initRequest = - new InitiateMultipartUploadRequest(bucketName, keyName); - InitiateMultipartUploadResult initResponse = s3Client.initiateMultipartUpload(initRequest); - String uploadId = initResponse.getUploadId(); - - // Upload a part - String partContent = "newcontent"; - byte[] partBytes = partContent.getBytes(StandardCharsets.UTF_8); - UploadPartRequest uploadRequest = new UploadPartRequest() - .withBucketName(bucketName) - .withKey(keyName) - .withUploadId(uploadId) - .withPartNumber(1) - .withInputStream(new ByteArrayInputStream(partBytes)) - .withPartSize(partBytes.length); - UploadPartResult uploadResult = s3Client.uploadPart(uploadRequest); - - // Complete with If-Match: wrong-etag (should fail) - List partETags = Collections.singletonList(uploadResult.getPartETag()); - CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest( - bucketName, keyName, uploadId, partETags); - completeRequest.putCustomRequestHeader("If-Match", "\"wrong-etag\""); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.completeMultipartUpload(completeRequest)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(412, ase.getStatusCode()); - assertEquals("PreconditionFailed", ase.getErrorCode()); - } - - @ParameterizedTest - @ValueSource(strings = {"*", "\"some-etag\""}) - public void testCompleteMultipartUploadIfMatchMissingKeyFail(String ifMatch) throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - - // Initiate multipart upload (key doesn't exist) - InitiateMultipartUploadRequest initRequest = - new InitiateMultipartUploadRequest(bucketName, keyName); - InitiateMultipartUploadResult initResponse = s3Client.initiateMultipartUpload(initRequest); - String uploadId = initResponse.getUploadId(); - - // Upload a part - String partContent = "newcontent"; - byte[] partBytes = partContent.getBytes(StandardCharsets.UTF_8); - UploadPartRequest uploadRequest = new UploadPartRequest() - .withBucketName(bucketName) - .withKey(keyName) - .withUploadId(uploadId) - .withPartNumber(1) - .withInputStream(new ByteArrayInputStream(partBytes)) - .withPartSize(partBytes.length); - UploadPartResult uploadResult = s3Client.uploadPart(uploadRequest); - - // Complete with If-Match on non-existent key (should fail) - List partETags = Collections.singletonList(uploadResult.getPartETag()); - CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest( - bucketName, keyName, uploadId, partETags); - completeRequest.putCustomRequestHeader("If-Match", ifMatch); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.completeMultipartUpload(completeRequest)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(404, ase.getStatusCode()); - assertEquals("NoSuchKey", ase.getErrorCode()); - } - - @Test - public void testPutDoubleSlashPrefixObject() throws IOException { - final String bucketName = getBucketName(); - final String keyName = "//dir1"; - final String content = "bar"; - // Create a FSO bucket for test - try (OzoneClient ozoneClient = cluster.newClient()) { - ObjectStore store = ozoneClient.getObjectStore(); - OzoneVolume volume = store.getS3Volume(); - OmBucketInfo.Builder bucketInfo = new OmBucketInfo.Builder() - .setVolumeName(volume.getName()) - .setBucketName(bucketName) - .setBucketLayout(BucketLayout.FILE_SYSTEM_OPTIMIZED); - OzoneManagerProtocol ozoneManagerProtocol = store.getClientProxy().getOzoneManagerClient(); - ozoneManagerProtocol.createBucket(bucketInfo.build()); - } - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName, is, new ObjectMetadata()); - assertEquals("37b51d194a7513e45b56f6524f2d51f2", putObjectResult.getETag()); - - S3Object object = s3Client.getObject(bucketName, keyName); - assertEquals(content.length(), object.getObjectMetadata().getContentLength()); - } - - @Test - public void testPutObjectEmpty() throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = ""; - s3Client.createBucket(bucketName); - - long initialAllocatedBlocks = - cluster.getStorageContainerManager().getPipelineManager() - .getMetrics().getTotalNumBlocksAllocated(); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - - PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName, is, new ObjectMetadata()); - assertEquals("d41d8cd98f00b204e9800998ecf8427e", putObjectResult.getETag()); - - // Verify via Ozone client that the key has no block locations - try (OzoneClient ozoneClient = cluster.newClient()) { - ObjectStore store = ozoneClient.getObjectStore(); - OzoneVolume volume = store.getS3Volume(); - OzoneBucket bucket = volume.getBucket(bucketName); - - OzoneKeyDetails keyDetails = bucket.getKey(keyName); - assertNotNull(keyDetails); - assertEquals(0, keyDetails.getDataSize(), - "Empty S3 object should have dataSize of 0"); - assertTrue(keyDetails.getOzoneKeyLocations().isEmpty(), - "Empty S3 object should have no block locations"); - } - - // createKey should skip block allocation if data size is 0 - long currentAllocatedBlocks = - cluster.getStorageContainerManager().getPipelineManager().getMetrics().getTotalNumBlocksAllocated(); - assertEquals(initialAllocatedBlocks, currentAllocatedBlocks); - } - - @Test - public void testPutObjectACL() throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - final byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); - s3Client.createBucket(bucketName); - - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - - PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName, is, new ObjectMetadata()); - String originalObjectETag = putObjectResult.getETag(); - assertTrue(s3Client.doesObjectExist(bucketName, keyName)); - - AccessControlList aclList = new AccessControlList(); - Owner owner = new Owner("owner", "owner"); - aclList.withOwner(owner); - Grantee grantee = new CanonicalGrantee("testGrantee"); - aclList.grantPermission(grantee, Permission.Read); - - SetObjectAclRequest setObjectAclRequest = new SetObjectAclRequest(bucketName, keyName, aclList); - - assertNotImplemented(() -> s3Client.setObjectAcl(setObjectAclRequest)); - - // Ensure that the object content remains unchanged - ObjectMetadata updatedObjectMetadata = s3Client.getObjectMetadata(bucketName, keyName); - assertEquals(originalObjectETag, updatedObjectMetadata.getETag()); - S3Object updatedObject = s3Client.getObject(bucketName, keyName); - - try (S3ObjectInputStream s3is = updatedObject.getObjectContent(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(contentBytes.length)) { - byte[] readBuf = new byte[1024]; - int readLen = 0; - while ((readLen = s3is.read(readBuf)) > 0) { - bos.write(readBuf, 0, readLen); - } - assertEquals(content, bos.toString("UTF-8")); - } - } - - @Test - public void testGetObjectAclNotImplemented() throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - s3Client.createBucket(bucketName); - s3Client.putObject( - bucketName, keyName, new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), new ObjectMetadata()); - - assertNotImplemented(() -> s3Client.getObjectAcl(new GetObjectAclRequest(bucketName, keyName))); - } - - @Test - public void testGetBucketAccelerateConfigurationNotImplemented() { - final String bucketName = getBucketName(); - s3Client.createBucket(bucketName); - - assertNotImplemented( - () -> s3Client.getBucketAccelerateConfiguration(new GetBucketAccelerateConfigurationRequest(bucketName))); - } - - @Test - public void testGetObject() throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "bar"; - final byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); - s3Client.createBucket(bucketName); - - InputStream is = new ByteArrayInputStream(contentBytes); - ObjectMetadata objectMetadata = new ObjectMetadata(); - Map userMetadata = new HashMap<>(); - userMetadata.put("key1", "value1"); - userMetadata.put("key2", "value2"); - objectMetadata.setUserMetadata(userMetadata); - - List tags = Arrays.asList(new Tag("tag1", "value1"), new Tag("tag2", "value2")); - ObjectTagging objectTagging = new ObjectTagging(tags); - - - PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, keyName, is, objectMetadata) - .withTagging(objectTagging); - - s3Client.putObject(putObjectRequest); - - S3Object s3Object = s3Client.getObject(bucketName, keyName); - assertEquals(tags.size(), s3Object.getTaggingCount()); - - try (S3ObjectInputStream s3is = s3Object.getObjectContent(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(contentBytes.length)) { - byte[] readBuf = new byte[1024]; - int readLen = 0; - while ((readLen = s3is.read(readBuf)) > 0) { - bos.write(readBuf, 0, readLen); - } - assertEquals(content, bos.toString("UTF-8")); - } - } - - /** - * Adapted from ceph s3-tests test_object_read_unreadable. - */ - @Test - public void testGetObjectUnreadableKey() { - final String bucketName = getBucketName(); - s3Client.createBucket(bucketName); - - String unreadableKey = new String(new byte[] {(byte) 0xae, (byte) 0x8a, '-'}, - StandardCharsets.ISO_8859_1); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.getObject(bucketName, unreadableKey)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(400, ase.getStatusCode()); - assertEquals(S3ErrorTable.INVALID_URI.getCode(), ase.getErrorCode()); - assertEquals(S3ErrorTable.INVALID_URI.getErrorMessage(), ase.getErrorMessage()); - } - - static Stream onlyTagKeyCasesV1() { - Map fooBarEmptyBar = new HashMap<>(); - fooBarEmptyBar.put("foo", "bar"); - fooBarEmptyBar.put("bar", ""); - return Stream.of( - Arguments.of( - new ObjectTagging(Collections.singletonList(new Tag("tag1", null))), - Collections.singletonMap("tag1", "")), - Arguments.of( - new ObjectTagging(Arrays.asList(new Tag("foo", "bar"), new Tag("bar", null))), - fooBarEmptyBar) - ); - } - - @ParameterizedTest - @MethodSource("onlyTagKeyCasesV1") - public void testPutObjectWithOnlyTagKey(ObjectTagging objectTagging, - Map expectedTags) throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "0123456789"; - s3Client.createBucket(bucketName); - - try (InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { - PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, keyName, is, new ObjectMetadata()) - .withTagging(objectTagging); - s3Client.putObject(putObjectRequest); - } - - GetObjectTaggingResult taggingResult = s3Client.getObjectTagging( - new GetObjectTaggingRequest(bucketName, keyName)); - Map actualTags = taggingResult.getTagSet().stream() - .collect(Collectors.toMap(Tag::getKey, Tag::getValue)); - assertEquals(expectedTags, actualTags); - } - - @Test - public void testHeadObjectReturnsTaggingCount() { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final String content = "head-object-tag-count"; - s3Client.createBucket(bucketName); - - s3Client.putObject(bucketName, keyName, - new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), new ObjectMetadata()); - - List tags = Arrays.asList(new Tag("tag1", "v1"), new Tag("tag2", "v2")); - s3Client.setObjectTagging( - new SetObjectTaggingRequest(bucketName, keyName, new ObjectTagging(tags))); - - ObjectMetadata head = s3Client.getObjectMetadata(bucketName, keyName); - // AWS SDK v1: getTaggingCount() exists on S3Object (GET), not on ObjectMetadata (HEAD). - // x-amz-tagging-count is exposed via raw metadata. - Object tagCountHeader = head.getRawMetadataValue(Headers.S3_TAGGING_COUNT); - assertNotNull(tagCountHeader); - assertEquals(tags.size(), Integer.parseInt(tagCountHeader.toString())); - } - - @Test - public void testGetObjectTaggingReturnsTagsSortedByKey() { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - s3Client.createBucket(bucketName); - s3Client.putObject(bucketName, keyName, ""); - - List tagsPutOrder = Arrays.asList(new Tag("key2", "val2"), new Tag("key", "val")); - s3Client.setObjectTagging( - new SetObjectTaggingRequest(bucketName, keyName, new ObjectTagging(tagsPutOrder))); - - GetObjectTaggingResult taggingResult = - s3Client.getObjectTagging(new GetObjectTaggingRequest(bucketName, keyName)); - List tagSet = taggingResult.getTagSet(); - assertEquals(2, tagSet.size()); - assertEquals("key", tagSet.get(0).getKey()); - assertEquals("val", tagSet.get(0).getValue()); - assertEquals("key2", tagSet.get(1).getKey()); - assertEquals("val2", tagSet.get(1).getValue()); - } - - @Test - public void testBucketTaggingPutGetDelete() { - final String bucketName = getBucketName(); - s3Client.createBucket(bucketName); - - // AWS SDK v1 returns null when no bucket tagging is configured. - assertNull(s3Client.getBucketTaggingConfiguration( - new GetBucketTaggingConfigurationRequest(bucketName))); - - TagSet tagSet = new TagSet(); - tagSet.setTag("tag-key1", "tag-value1"); - tagSet.setTag("tag-key2", "tag-value2"); - s3Client.setBucketTaggingConfiguration(new SetBucketTaggingConfigurationRequest(bucketName, - new BucketTaggingConfiguration(Collections.singletonList(tagSet)))); - - BucketTaggingConfiguration taggingConfiguration = - s3Client.getBucketTaggingConfiguration(new GetBucketTaggingConfigurationRequest(bucketName)); - Map actualTags = taggingConfiguration.getTagSet().getAllTags(); - assertEquals(2, actualTags.size()); - assertEquals("tag-value1", actualTags.get("tag-key1")); - assertEquals("tag-value2", actualTags.get("tag-key2")); - - s3Client.deleteBucketTaggingConfiguration(new DeleteBucketTaggingConfigurationRequest(bucketName)); - - assertNull(s3Client.getBucketTaggingConfiguration( - new GetBucketTaggingConfigurationRequest(bucketName))); - } - - @Test - public void testGetObjectWithoutETag() throws Exception { - // Object uploaded using other protocols (e.g. ofs / ozone cli) will not - // have ETag. Ensure that ETag will not do ETag validation on GetObject if there - // is no ETag present. - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - - s3Client.createBucket(bucketName); - - String value = "sample value"; - byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8); - - try (OzoneClient ozoneClient = cluster.newClient()) { - ObjectStore store = ozoneClient.getObjectStore(); - - OzoneVolume volume = store.getS3Volume(); - OzoneBucket bucket = volume.getBucket(bucketName); - - try (OzoneOutputStream out = bucket.createKey(keyName, - valueBytes.length, - ReplicationConfig.fromTypeAndFactor(ReplicationType.RATIS, ReplicationFactor.ONE), - Collections.emptyMap())) { - out.write(valueBytes); - } - } - - S3Object s3Object = s3Client.getObject(bucketName, keyName); - assertNull(s3Object.getObjectMetadata().getETag()); - - try (S3ObjectInputStream s3is = s3Object.getObjectContent(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(valueBytes.length)) { - byte[] readBuf = new byte[1024]; - int readLen = 0; - while ((readLen = s3is.read(readBuf)) > 0) { - bos.write(readBuf, 0, readLen); - } - assertEquals(value, bos.toString("UTF-8")); - } - } - - @Test - public void testListObjectsMany() throws Exception { - testListObjectsMany(false); - } - - @Test - public void testListObjectsManyV2() throws Exception { - testListObjectsMany(true); - } - - @Test - public void testListObjectsSpecialKeyNames() throws Exception { - final String bucketName = getBucketName("special-keys"); - final String content = "x"; - s3Client.createBucket(bucketName); - - for (String keyName : S3SDKTestUtils.S3_SPECIAL_KEY_NAMES) { - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - s3Client.putObject(bucketName, keyName, is, new ObjectMetadata()); - try (S3Object object = s3Client.getObject(bucketName, keyName)) { - assertEquals(content, IOUtils.toString(object.getObjectContent(), StandardCharsets.UTF_8)); - } - } - - ObjectListing listObjectsResponse = s3Client.listObjects( - new ListObjectsRequest().withBucketName(bucketName)); - List listedKeys = listObjectsResponse.getObjectSummaries().stream() - .map(S3ObjectSummary::getKey) - .collect(Collectors.toList()); - assertEquals(S3SDKTestUtils.S3_SPECIAL_KEY_NAMES, listedKeys); - } - - private void testListObjectsMany(boolean isListV2) throws Exception { - final String bucketName = getBucketName(); - s3Client.createBucket(bucketName); - final List keyNames = Arrays.asList( - getKeyName("1"), - getKeyName("2"), - getKeyName("3") - ); - final List keyNamesWithoutETag = Arrays.asList( - getKeyName("4"), - getKeyName("5") - ); - - final Map keyToEtag = new HashMap<>(); - for (String keyName: keyNames) { - PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName, - RandomStringUtils.secure().nextAlphanumeric(5)); - keyToEtag.put(keyName, putObjectResult.getETag()); - } - try (OzoneClient ozoneClient = cluster.newClient()) { - ObjectStore store = ozoneClient.getObjectStore(); - - OzoneVolume volume = store.getS3Volume(); - OzoneBucket bucket = volume.getBucket(bucketName); - - for (String keyNameWithoutETag : keyNamesWithoutETag) { - byte[] valueBytes = RandomStringUtils.secure().nextAlphanumeric(5).getBytes(StandardCharsets.UTF_8); - try (OzoneOutputStream out = bucket.createKey(keyNameWithoutETag, - valueBytes.length, - ReplicationConfig.fromTypeAndFactor(ReplicationType.RATIS, ReplicationFactor.ONE), - Collections.emptyMap())) { - out.write(valueBytes); - } - } - } - - List objectSummaries; - String continuationToken; - if (isListV2) { - ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request() - .withBucketName(bucketName) - .withMaxKeys(2); - ListObjectsV2Result listObjectsResponse = s3Client.listObjectsV2(listObjectsRequest); - objectSummaries = listObjectsResponse.getObjectSummaries(); - assertEquals(bucketName, listObjectsResponse.getBucketName()); - assertTrue(listObjectsResponse.isTruncated()); - continuationToken = listObjectsResponse.getNextContinuationToken(); - } else { - ListObjectsRequest listObjectsRequest = new ListObjectsRequest() - .withBucketName(bucketName) - .withMaxKeys(2); - ObjectListing listObjectsResponse = s3Client.listObjects(listObjectsRequest); - objectSummaries = listObjectsResponse.getObjectSummaries(); - assertEquals(bucketName, listObjectsResponse.getBucketName()); - assertTrue(listObjectsResponse.isTruncated()); - continuationToken = listObjectsResponse.getNextMarker(); - } - assertThat(objectSummaries).hasSize(2); - assertEquals(objectSummaries.stream() - .map(S3ObjectSummary::getKey).collect(Collectors.toList()), - keyNames.subList(0, 2)); - for (S3ObjectSummary objectSummary : objectSummaries) { - assertEquals(keyToEtag.get(objectSummary.getKey()), objectSummary.getETag()); - } - - // Include both keys with and without ETag - if (isListV2) { - ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request() - .withBucketName(bucketName) - .withMaxKeys(5) - .withContinuationToken(continuationToken); - ListObjectsV2Result listObjectsResponse = s3Client.listObjectsV2(listObjectsRequest); - objectSummaries = listObjectsResponse.getObjectSummaries(); - assertEquals(bucketName, listObjectsResponse.getBucketName()); - assertFalse(listObjectsResponse.isTruncated()); - } else { - ListObjectsRequest listObjectsRequest = new ListObjectsRequest() - .withBucketName(bucketName) - .withMaxKeys(5) - .withMarker(continuationToken); - ObjectListing listObjectsResponse = s3Client.listObjects(listObjectsRequest); - objectSummaries = listObjectsResponse.getObjectSummaries(); - assertEquals(bucketName, listObjectsResponse.getBucketName()); - assertFalse(listObjectsResponse.isTruncated()); - } - - assertThat(objectSummaries).hasSize(3); - assertEquals(keyNames.get(2), objectSummaries.get(0).getKey()); - assertEquals(keyNamesWithoutETag.get(0), objectSummaries.get(1).getKey()); - assertEquals(keyNamesWithoutETag.get(1), objectSummaries.get(2).getKey()); - for (S3ObjectSummary objectSummary : objectSummaries) { - assertEquals(keyToEtag.get(objectSummary.getKey()), objectSummary.getETag()); - } - } - - @Test - public void testListObjectsBucketNotExist() { - final String bucketName = getBucketName(); - ListObjectsRequest listObjectsRequest = new ListObjectsRequest() - .withBucketName(bucketName); - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.listObjects(listObjectsRequest)); - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(404, ase.getStatusCode()); - assertEquals("NoSuchBucket", ase.getErrorCode()); - } - - @Test - public void testListObjectsV2BucketNotExist() { - final String bucketName = getBucketName(); - ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request() - .withBucketName(bucketName); - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.listObjectsV2(listObjectsRequest)); - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(404, ase.getStatusCode()); - assertEquals("NoSuchBucket", ase.getErrorCode()); - } - - @Test - public void testListObjectsV2FetchOwner() { - final String bucketName = getBucketName("fetch-owner"); - final String keyName = getKeyName("obj"); - s3Client.createBucket(bucketName); - s3Client.putObject(bucketName, keyName, RandomStringUtils.secure().nextAlphanumeric(5)); - - ListObjectsV2Result defaultResponse = s3Client.listObjectsV2( - new ListObjectsV2Request().withBucketName(bucketName)); - assertThat(defaultResponse.getObjectSummaries()).isNotEmpty(); - assertNull(defaultResponse.getObjectSummaries().get(0).getOwner()); - - ListObjectsV2Result falseResponse = s3Client.listObjectsV2( - new ListObjectsV2Request().withBucketName(bucketName).withFetchOwner(false)); - assertNull(falseResponse.getObjectSummaries().get(0).getOwner()); - - ListObjectsV2Result trueResponse = s3Client.listObjectsV2( - new ListObjectsV2Request().withBucketName(bucketName).withFetchOwner(true)); - Owner owner = trueResponse.getObjectSummaries().get(0).getOwner(); - assertNotNull(owner); - assertNotNull(owner.getDisplayName()); - assertEquals(S3Owner.DEFAULT_S3OWNER_ID, owner.getId()); - } - - @Test - public void testHighLevelMultipartUpload(@TempDir Path tempDir) throws Exception { - TransferManager tm = TransferManagerBuilder.standard() - .withS3Client(s3Client) - .build(); - - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - - s3Client.createBucket(bucketName); - - // The minimum file size to for TransferManager to initiate multipart upload is 16MB, so create a file - // larger than the threshold. - // See TransferManagerConfiguration#getMultipartUploadThreshold - int fileSize = (int) (20 * MB); - File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); - - createFile(multipartUploadFile, fileSize); - - // TransferManager processes all transfers asynchronously, - // so this call returns immediately. - Upload upload = tm.upload(bucketName, keyName, multipartUploadFile); - - upload.waitForCompletion(); - UploadResult uploadResult = upload.waitForUploadResult(); - assertEquals(bucketName, uploadResult.getBucketName()); - assertEquals(keyName, uploadResult.getKey()); - } - - @Test - public void testLowLevelMultipartUpload(@TempDir Path tempDir) throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final Map userMetadata = new HashMap<>(); - userMetadata.put("key1", "value1"); - userMetadata.put("key2", "value2"); - - List tags = Arrays.asList(new Tag("tag1", "value1"), new Tag("tag2", "value2")); - - s3Client.createBucket(bucketName); - - File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); - - createFile(multipartUploadFile, (int) (25 * MB)); - - multipartUpload(bucketName, keyName, multipartUploadFile, 5 * MB, null, userMetadata, tags); - - S3Object s3Object = s3Client.getObject(bucketName, keyName); - assertEquals(keyName, s3Object.getKey()); - assertEquals(bucketName, s3Object.getBucketName()); - assertEquals(tags.size(), s3Object.getTaggingCount()); - - ObjectMetadata objectMetadata = s3Client.getObjectMetadata(bucketName, keyName); - assertEquals(userMetadata, objectMetadata.getUserMetadata()); - } - - @Test - public void testListMultipartUploads() { - final String bucketName = getBucketName(); - final String multipartKey1 = getKeyName("multipart1"); - final String multipartKey2 = getKeyName("multipart2"); - - s3Client.createBucket(bucketName); - - List uploadIds = new ArrayList<>(); - - String uploadId1 = initiateMultipartUpload(bucketName, multipartKey1, null, null, null); - uploadIds.add(uploadId1); - String uploadId2 = initiateMultipartUpload(bucketName, multipartKey1, null, null, null); - uploadIds.add(uploadId2); - - String uploadId3 = initiateMultipartUpload(bucketName, multipartKey2, null, null, null); - uploadIds.add(uploadId3); - - ListMultipartUploadsRequest listMultipartUploadsRequest = new ListMultipartUploadsRequest(bucketName); - listMultipartUploadsRequest.setMaxUploads(5000); - - MultipartUploadListing result = s3Client.listMultipartUploads(listMultipartUploadsRequest); - - List listUploadIds = result.getMultipartUploads().stream() - .map(MultipartUpload::getUploadId) - .collect(Collectors.toList()); - - assertEquals(uploadIds, listUploadIds); - } - - @ParameterizedTest - @ValueSource(ints = {10, 5000}) - public void testListMultipartUploadsPagination(int requestedMaxUploads) { - final String bucketName = getBucketName() + "-" + requestedMaxUploads; - final String multipartKeyPrefix = getKeyName("multipart"); - - s3Client.createBucket(bucketName); - - // Create multipart uploads to test pagination - List allKeys = new ArrayList<>(); - Map keyToUploadId = new HashMap<>(); - - final int effectiveMaxUploads = Math.min(requestedMaxUploads, MAX_UPLOADS_LIMIT); - final int uploadsCreated = 2 * effectiveMaxUploads + 5; - final int expectedPages = uploadsCreated / effectiveMaxUploads + 1; - - for (int i = 0; i < uploadsCreated; i++) { - String key = String.format("%s-%04d", multipartKeyPrefix, i); - allKeys.add(key); - String uploadId = initiateMultipartUpload(bucketName, key, null, null, null); - keyToUploadId.put(key, uploadId); - } - Collections.sort(allKeys); - - // Test pagination - Set retrievedKeys = new HashSet<>(); - String keyMarker = null; - String uploadIdMarker = null; - boolean truncated = true; - int pageCount = 0; - - do { - ListMultipartUploadsRequest request = new ListMultipartUploadsRequest(bucketName) - .withMaxUploads(requestedMaxUploads) - .withKeyMarker(keyMarker) - .withUploadIdMarker(uploadIdMarker); - - MultipartUploadListing result = s3Client.listMultipartUploads(request); - pageCount++; - - // Verify page size - if (pageCount < expectedPages) { - assertEquals(effectiveMaxUploads, result.getMultipartUploads().size()); - assertTrue(result.isTruncated()); - } else { - assertEquals(uploadsCreated % effectiveMaxUploads, result.getMultipartUploads().size()); - assertFalse(result.isTruncated()); - } - - // Collect keys and verify uploadIds - for (MultipartUpload upload : result.getMultipartUploads()) { - String key = upload.getKey(); - retrievedKeys.add(key); - assertEquals(keyToUploadId.get(key), upload.getUploadId()); - } - - // Verify response - assertNull(result.getPrefix()); - assertEquals(result.getUploadIdMarker(), uploadIdMarker); - assertEquals(result.getKeyMarker(), keyMarker); - assertEquals(effectiveMaxUploads, result.getMaxUploads()); - - // Verify next markers content - if (result.isTruncated()) { - MultipartUpload lastUploadOnPage = result.getMultipartUploads() - .get(result.getMultipartUploads().size() - 1); - assertEquals(lastUploadOnPage.getKey(), result.getNextKeyMarker()); - assertEquals(lastUploadOnPage.getUploadId(), result.getNextUploadIdMarker()); - } else { - assertNull(result.getNextKeyMarker()); - assertNull(result.getNextUploadIdMarker()); - } - - // Update markers for next page - keyMarker = result.getNextKeyMarker(); - uploadIdMarker = result.getNextUploadIdMarker(); - - truncated = result.isTruncated(); - - } while (truncated); - - // Verify pagination results - assertEquals(expectedPages, pageCount); - assertEquals(uploadsCreated, retrievedKeys.size(), "Should retrieve all uploads"); - assertEquals( - allKeys, - retrievedKeys.stream().sorted().collect(Collectors.toList()), - "Retrieved keys should match expected keys in order"); - - // Test with prefix - String prefix = multipartKeyPrefix + "-001"; - ListMultipartUploadsRequest prefixRequest = new ListMultipartUploadsRequest(bucketName) - .withPrefix(prefix); - - MultipartUploadListing prefixResult = s3Client.listMultipartUploads(prefixRequest); - - assertEquals(prefix, prefixResult.getPrefix()); - assertEquals( - IntStream.rangeClosed(0, 9) - .mapToObj(i -> prefix + i) - .collect(Collectors.toList()), - prefixResult.getMultipartUploads().stream() - .map(MultipartUpload::getKey) - .collect(Collectors.toList())); - } - - @Test - public void testListMultipartUploadsPaginationCornerCases() { - final String bucketName = getBucketName(); - final String keyA = getKeyName("samekey"); - final String keyB = getKeyName("after"); - - s3Client.createBucket(bucketName); - - // Create multiple MPUs for the same key to verify upload-id-marker semantics - List keyAUploadIds = new ArrayList<>(); - keyAUploadIds.add(initiateMultipartUpload(bucketName, keyA, null, null, null)); - keyAUploadIds.add(initiateMultipartUpload(bucketName, keyA, null, null, null)); - keyAUploadIds.add(initiateMultipartUpload(bucketName, keyA, null, null, null)); - // Also create another key to ensure listing proceeds past keyA - initiateMultipartUpload(bucketName, keyB, null, null, null); - - // Sort upload IDs lexicographically to match listing order for the same key - Collections.sort(keyAUploadIds); - - // Case 1: key-marker=keyA and upload-id-marker set to the lowest uploadId - // Per spec, same-key uploads MAY be included if uploadId > marker - ListMultipartUploadsRequest request1 = new ListMultipartUploadsRequest(bucketName) - .withKeyMarker(keyA) - .withUploadIdMarker(keyAUploadIds.get(0)) - .withMaxUploads(100); - MultipartUploadListing result1 = s3Client.listMultipartUploads(request1); - - List uploads1 = result1.getMultipartUploads(); - // Collect same-key uploads and verify none are <= marker - List sameKeyIds1 = uploads1.stream() - .filter(u -> keyA.equals(u.getKey())) - .map(MultipartUpload::getUploadId) - .collect(Collectors.toList()); - assertThat(sameKeyIds1).allSatisfy(id -> assertTrue(id.compareTo(keyAUploadIds.get(0)) > 0)); - - // Case 2: key-marker=keyA and upload-id-marker set to the highest uploadId - // Expect no same-key (keyA) uploads to be returned - ListMultipartUploadsRequest request2 = new ListMultipartUploadsRequest(bucketName) - .withKeyMarker(keyA) - .withUploadIdMarker(keyAUploadIds.get(2)) - .withMaxUploads(100); - MultipartUploadListing result2 = s3Client.listMultipartUploads(request2); - assertTrue(result2.getMultipartUploads().stream().noneMatch(u -> keyA.equals(u.getKey()))); - } - - @Test - public void testListParts(@TempDir Path tempDir) throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - final long fileSize = 5 * MB; - final long partSize = 1 * MB; - final int maxParts = 2; - - s3Client.createBucket(bucketName); - - String uploadId = initiateMultipartUpload(bucketName, keyName, null, null, null); - - File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); - - createFile(multipartUploadFile, (int) fileSize); - - List partETags = uploadParts(bucketName, keyName, uploadId, multipartUploadFile, partSize); - - List listPartETags = new ArrayList<>(); - int partNumberMarker = 0; - int expectedNumOfParts = 5; - PartListing listPartsResult; - do { - ListPartsRequest listPartsRequest = new ListPartsRequest(bucketName, keyName, uploadId) - .withMaxParts(maxParts) - .withPartNumberMarker(partNumberMarker); - listPartsResult = s3Client.listParts(listPartsRequest); - if (expectedNumOfParts > maxParts) { - assertTrue(listPartsResult.isTruncated()); - partNumberMarker = listPartsResult.getNextPartNumberMarker(); - expectedNumOfParts -= maxParts; - } else { - assertFalse(listPartsResult.isTruncated()); - } - for (PartSummary partSummary : listPartsResult.getParts()) { - listPartETags.add(new PartETag(partSummary.getPartNumber(), partSummary.getETag())); - } - } while (listPartsResult.isTruncated()); - - assertEquals(partETags.size(), listPartETags.size()); - for (int i = 0; i < partETags.size(); i++) { - assertEquals(partETags.get(i).getPartNumber(), listPartETags.get(i).getPartNumber()); - assertEquals(partETags.get(i).getETag(), listPartETags.get(i).getETag()); - } - } - - @Test - public void testGetParticularPart(@TempDir Path tempDir) throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - - s3Client.createBucket(bucketName); - - File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); - - createFile(multipartUploadFile, (int) (15 * MB)); - - multipartUpload(bucketName, keyName, multipartUploadFile, 5 * MB, null, null, null); - - GetObjectRequest getObjectRequestAll = new GetObjectRequest(bucketName, keyName); - getObjectRequestAll.setPartNumber(0); - S3Object s3ObjectAll = s3Client.getObject(getObjectRequestAll); - long allPartContentLength = s3ObjectAll.getObjectMetadata().getContentLength(); - - GetObjectRequest getObjectRequestOne = new GetObjectRequest(bucketName, keyName); - getObjectRequestOne.setPartNumber(1); - S3Object s3ObjectOne = s3Client.getObject(getObjectRequestOne); - long partOneContentLength = s3ObjectOne.getObjectMetadata().getContentLength(); - assertEquals(allPartContentLength / 3, partOneContentLength); - - GetObjectRequest getObjectRequestTwo = new GetObjectRequest(bucketName, keyName); - getObjectRequestTwo.setPartNumber(2); - S3Object s3ObjectTwo = s3Client.getObject(getObjectRequestTwo); - long partTwoContentLength = s3ObjectTwo.getObjectMetadata().getContentLength(); - assertEquals(allPartContentLength / 3, partTwoContentLength); - - GetObjectRequest getObjectRequestThree = new GetObjectRequest(bucketName, keyName); - getObjectRequestThree.setPartNumber(1); - S3Object s3ObjectThree = s3Client.getObject(getObjectRequestTwo); - long partThreeContentLength = s3ObjectThree.getObjectMetadata().getContentLength(); - assertEquals(allPartContentLength / 3, partThreeContentLength); - - assertEquals(allPartContentLength, (partOneContentLength + partTwoContentLength + partThreeContentLength)); - } - - @Test - public void testGetNotExistedPart(@TempDir Path tempDir) throws Exception { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - - s3Client.createBucket(bucketName); - - File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); - - createFile(multipartUploadFile, (int) (15 * MB)); - - multipartUpload(bucketName, keyName, multipartUploadFile, 5 * MB, null, null, null); - - GetObjectRequest getObjectRequestOne = new GetObjectRequest(bucketName, keyName); - getObjectRequestOne.setPartNumber(4); - // Reading a part number beyond the object's part count must fail with - // InvalidPart, instead of returning an empty (0-byte) object. - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.getObject(getObjectRequestOne)); - assertEquals(400, ase.getStatusCode()); - assertEquals("InvalidPart", ase.getErrorCode()); - } - - @Test - public void testListPartsNotFound() { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - - s3Client.createBucket(bucketName); - - ListPartsRequest listPartsRequest = - new ListPartsRequest(bucketName, keyName, "nonexist"); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.listParts(listPartsRequest)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(404, ase.getStatusCode()); - assertEquals("NoSuchUpload", ase.getErrorCode()); - } - - @Test - public void testQuotaExceeded() throws IOException { - final String bucketName = getBucketName(); - final String keyName = getKeyName(); - - s3Client.createBucket(bucketName); - - cluster.newClient().getObjectStore() - .getVolume("s3v") - .getBucket(bucketName) - .setQuota(OzoneQuota.parseQuota("1", "10")); - - // Upload some objects to the bucket - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.putObject(bucketName, keyName, - RandomStringUtils.secure().nextAlphanumeric(1024))); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(403, ase.getStatusCode()); - assertEquals("QuotaExceeded", ase.getErrorCode()); - } - - @Test - public void testS3LifecycleConfigurationCreateSuccessfully() { - final String bucketName = getBucketName(); - s3Client.createBucket(bucketName); - BucketLifecycleConfiguration configuration = new BucketLifecycleConfiguration(); - List rules = new ArrayList<>(); - BucketLifecycleConfiguration.Rule rule1 = new BucketLifecycleConfiguration.Rule() - .withId("expire-logs-after-365-days") - .withPrefix("logs/") - .withStatus(BucketLifecycleConfiguration.ENABLED) - .withExpirationInDays(365); - rules.add(rule1); - - configuration.setRules(rules); - - // Set lifecycle configuration - SetBucketLifecycleConfigurationRequest request = - new SetBucketLifecycleConfigurationRequest(bucketName, configuration); - s3Client.setBucketLifecycleConfiguration(request); - - // Verify the configuration was set - BucketLifecycleConfiguration retrievedConfig = - s3Client.getBucketLifecycleConfiguration(bucketName); - assertEquals(1, retrievedConfig.getRules().size()); - - // Verify rule 1 - BucketLifecycleConfiguration.Rule retrievedRule1 = retrievedConfig.getRules().get(0); - assertEquals("expire-logs-after-365-days", retrievedRule1.getId()); - assertEquals("logs/", retrievedRule1.getPrefix()); - assertEquals(BucketLifecycleConfiguration.ENABLED, retrievedRule1.getStatus()); - assertEquals(365, retrievedRule1.getExpirationInDays()); - } - - @Test - public void testS3LifecycleConfigurationCreationFailed() { - final String bucketName = getBucketName(); - s3Client.createBucket(bucketName); - - // Test 1: Invalid configuration - BucketLifecycleConfiguration configuration = new BucketLifecycleConfiguration(); - List rules = new ArrayList<>(); - - BucketLifecycleConfiguration.Rule rule = new BucketLifecycleConfiguration.Rule() - .withId("invalid") - .withStatus(BucketLifecycleConfiguration.ENABLED); - rules.add(rule); - configuration.setRules(rules); - SetBucketLifecycleConfigurationRequest request = - new SetBucketLifecycleConfigurationRequest(bucketName, configuration); - - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.setBucketLifecycleConfiguration(request)); - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, ase.getStatusCode()); - - // Test 2: Non-existent bucket - final String nonExistentBucket = getBucketName("nonexistent"); - BucketLifecycleConfiguration validConfig = new BucketLifecycleConfiguration(); - List validRules = new ArrayList<>(); - BucketLifecycleConfiguration.Rule validRule = new BucketLifecycleConfiguration.Rule() - .withId("test-rule") - .withPrefix("test/") - .withStatus(BucketLifecycleConfiguration.ENABLED) - .withExpirationInDays(30); - validRules.add(validRule); - validConfig.setRules(validRules); - - SetBucketLifecycleConfigurationRequest nonExistentRequest = - new SetBucketLifecycleConfigurationRequest(nonExistentBucket, validConfig); - - AmazonServiceException ase2 = assertThrows(AmazonServiceException.class, - () -> s3Client.setBucketLifecycleConfiguration(nonExistentRequest)); - assertEquals(ErrorType.Client, ase2.getErrorType()); - assertEquals(HttpURLConnection.HTTP_NOT_FOUND, ase2.getStatusCode()); - assertEquals("NoSuchBucket", ase2.getErrorCode()); - - BucketLifecycleConfiguration pastDateConfig = new BucketLifecycleConfiguration(); - List pastDateRules = new ArrayList<>(); - BucketLifecycleConfiguration.Rule pastDateRule = new BucketLifecycleConfiguration.Rule() - .withId("past-date") - .withPrefix("logs/") - .withStatus(BucketLifecycleConfiguration.ENABLED) - .withExpirationDate(Date.from(Instant.parse("2020-01-01T00:00:00Z"))); - pastDateRules.add(pastDateRule); - pastDateConfig.setRules(pastDateRules); - SetBucketLifecycleConfigurationRequest pastDateRequest = - new SetBucketLifecycleConfigurationRequest(bucketName, pastDateConfig); - - AmazonServiceException ase3 = assertThrows(AmazonServiceException.class, - () -> s3Client.setBucketLifecycleConfiguration(pastDateRequest)); - assertEquals(ErrorType.Client, ase3.getErrorType()); - assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, ase3.getStatusCode()); - assertEquals(S3ErrorTable.INVALID_REQUEST.getCode(), ase3.getErrorCode()); - assertTrue(ase3.getErrorMessage().contains("must be in the future"), - "The client should receive OM's detailed message explaining the date is in the past, got: " - + ase3.getErrorMessage()); - } - - @Test - public void testS3LifecycleConfigurationDelete() { - final String bucketName = getBucketName(); - s3Client.createBucket(bucketName); - - // Test delete lifecycle for a bucket, while doesn't have lifecycle - assertNull(s3Client.getBucketLifecycleConfiguration(bucketName)); - // Idempotent delete: no exception expected even without an existing config - s3Client.deleteBucketLifecycleConfiguration(bucketName); - - // First create a lifecycle configuration - BucketLifecycleConfiguration configuration = new BucketLifecycleConfiguration(); - List rules = new ArrayList<>(); - BucketLifecycleConfiguration.Rule rule = new BucketLifecycleConfiguration.Rule() - .withId("test-rule") - .withPrefix("test/") - .withStatus(BucketLifecycleConfiguration.ENABLED) - .withExpirationInDays(30); - rules.add(rule); - configuration.setRules(rules); - - s3Client.setBucketLifecycleConfiguration(bucketName, configuration); - // Verify it exists - BucketLifecycleConfiguration retrievedConfig = - s3Client.getBucketLifecycleConfiguration(bucketName); - assertEquals(1, retrievedConfig.getRules().size()); - // Delete the lifecycle configuration - s3Client.deleteBucketLifecycleConfiguration(bucketName); - assertNull(s3Client.getBucketLifecycleConfiguration(bucketName)); - // Test delete on non-existent bucket - final String nonExistentBucket = getBucketName("nonexistent"); - assertThrows(AmazonServiceException.class, - () -> s3Client.deleteBucketLifecycleConfiguration(nonExistentBucket)); - } - - @Test - public void testS3LifecycleConfigurationGet() { - final String bucketName = getBucketName(); - s3Client.createBucket(bucketName); - - // Test get on bucket without lifecycle configuration - assertNull(s3Client.getBucketLifecycleConfiguration(bucketName)); - - // Create a comprehensive lifecycle configuration - BucketLifecycleConfiguration configuration = new BucketLifecycleConfiguration(); - List rules = new ArrayList<>(); - - // Rule with expiration and prefix - BucketLifecycleConfiguration.Rule rule1 = new BucketLifecycleConfiguration.Rule() - .withId("expire-old-files") - .withPrefix("old/") - .withStatus(BucketLifecycleConfiguration.ENABLED) - .withExpirationInDays(365); - - rules.add(rule1); - configuration.setRules(rules); - - // Set the configuration - s3Client.setBucketLifecycleConfiguration(bucketName, configuration); - - // Get and verify the configuration - BucketLifecycleConfiguration retrievedConfig = - s3Client.getBucketLifecycleConfiguration(bucketName); - - assertEquals(1, retrievedConfig.getRules().size()); - - // Verify first rule - BucketLifecycleConfiguration.Rule retrievedRule1 = retrievedConfig.getRules().get(0); - assertEquals("expire-old-files", retrievedRule1.getId()); - assertEquals("old/", retrievedRule1.getPrefix()); - assertEquals(BucketLifecycleConfiguration.ENABLED, retrievedRule1.getStatus()); - assertEquals(365, retrievedRule1.getExpirationInDays()); - - // Test getting configuration using GetBucketLifecycleConfigurationRequest - GetBucketLifecycleConfigurationRequest getRequest = - new GetBucketLifecycleConfigurationRequest(bucketName); - BucketLifecycleConfiguration configFromRequest = - s3Client.getBucketLifecycleConfiguration(getRequest); - assertEquals(retrievedConfig.getRules().size(), configFromRequest.getRules().size()); - } - - @Test - public void testGetLifecycleWithAbortIncompleteMultipartUpload() { - final String bucketName = getBucketName(); - s3Client.createBucket(bucketName); - - BucketLifecycleConfiguration configuration = new BucketLifecycleConfiguration(); - List rules = new ArrayList<>(); - - BucketLifecycleConfiguration.Rule rule1 = new BucketLifecycleConfiguration.Rule() - .withId("abort-incomplete-mpu-with-prefix") - .withPrefix("uploads/") - .withStatus(BucketLifecycleConfiguration.ENABLED); - rule1.setAbortIncompleteMultipartUpload( - new AbortIncompleteMultipartUpload().withDaysAfterInitiation(7)); - - BucketLifecycleConfiguration.Rule rule2 = new BucketLifecycleConfiguration.Rule() - .withId("abort-incomplete-mpu-temp") - .withPrefix("temp/") - .withStatus(BucketLifecycleConfiguration.ENABLED); - rule2.setAbortIncompleteMultipartUpload( - new AbortIncompleteMultipartUpload().withDaysAfterInitiation(3)); - - BucketLifecycleConfiguration.Rule rule3 = new BucketLifecycleConfiguration.Rule() - .withId("abort-incomplete-mpu-no-prefix") - .withPrefix("") - .withStatus(BucketLifecycleConfiguration.ENABLED); - rule3.setAbortIncompleteMultipartUpload( - new AbortIncompleteMultipartUpload().withDaysAfterInitiation(29)); - - rules.add(rule1); - rules.add(rule2); - rules.add(rule3); - configuration.setRules(rules); - - // Set lifecycle configuration - s3Client.setBucketLifecycleConfiguration(bucketName, configuration); - - // Get and verify the configuration - BucketLifecycleConfiguration retrievedConfig = - s3Client.getBucketLifecycleConfiguration(bucketName); - - assertEquals(3, retrievedConfig.getRules().size()); - - BucketLifecycleConfiguration.Rule retrievedRule1 = retrievedConfig.getRules().get(0); - assertEquals("abort-incomplete-mpu-with-prefix", retrievedRule1.getId()); - assertEquals("uploads/", retrievedRule1.getPrefix()); - assertEquals(BucketLifecycleConfiguration.ENABLED, retrievedRule1.getStatus()); - assertEquals(7, retrievedRule1.getAbortIncompleteMultipartUpload().getDaysAfterInitiation()); - - BucketLifecycleConfiguration.Rule retrievedRule2 = retrievedConfig.getRules().get(1); - assertEquals("abort-incomplete-mpu-temp", retrievedRule2.getId()); - assertEquals("temp/", retrievedRule2.getPrefix()); - assertEquals(BucketLifecycleConfiguration.ENABLED, retrievedRule2.getStatus()); - assertEquals(3, retrievedRule2.getAbortIncompleteMultipartUpload().getDaysAfterInitiation()); - - BucketLifecycleConfiguration.Rule retrievedRule3 = retrievedConfig.getRules().get(2); - assertEquals("abort-incomplete-mpu-no-prefix", retrievedRule3.getId()); - assertEquals("", retrievedRule3.getPrefix()); - assertEquals(BucketLifecycleConfiguration.ENABLED, retrievedRule3.getStatus()); - assertEquals(29, retrievedRule3.getAbortIncompleteMultipartUpload().getDaysAfterInitiation()); - } - - /** - * End-to-end test verifying that KeyLifecycleService correctly aborts - * incomplete multipart uploads based on lifecycle configuration. - */ - @Test - void testAbortIncompleteMultipartUploadE2E() throws Exception { - OzoneManager ozoneManager = cluster().getOzoneManager(); - KeyLifecycleService lifecycleService = ozoneManager.getKeyManager().getKeyLifecycleService(); - if (lifecycleService == null) { - return; - } - - final String bucketName = getBucketName(); - s3Client.createBucket(bucketName); - - String s3VolumeName; - try (OzoneClient ozoneClient = cluster().newClient()) { - s3VolumeName = ozoneClient.getObjectStore().getS3Volume().getName(); - } - - OMMetadataManager metadataManager = ozoneManager.getMetadataManager(); - - // Create 3 MPUs - String matchingOldKey = "temp/old-file.txt"; - InitiateMultipartUploadResult mpu1 = s3Client.initiateMultipartUpload( - new InitiateMultipartUploadRequest(bucketName, matchingOldKey)); - - String nonMatchingOldKey = "permanent/old-file.txt"; - InitiateMultipartUploadResult mpu2 = s3Client.initiateMultipartUpload( - new InitiateMultipartUploadRequest(bucketName, nonMatchingOldKey)); - - String matchingRecentKey = "temp/recent-file.txt"; - s3Client.initiateMultipartUpload(new InitiateMultipartUploadRequest(bucketName, matchingRecentKey)); - - MultipartUploadListing listBefore = s3Client.listMultipartUploads( - new ListMultipartUploadsRequest(bucketName)); - assertEquals(3, listBefore.getMultipartUploads().size()); - - // Backdate 2 MPUs to 2 days ago - long oldCreationTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2); - updateMpuCreationTime(metadataManager, s3VolumeName, bucketName, - matchingOldKey, mpu1.getUploadId(), oldCreationTime); - updateMpuCreationTime(metadataManager, s3VolumeName, bucketName, - nonMatchingOldKey, mpu2.getUploadId(), oldCreationTime); - - // Set lifecycle rule: abort MPUs with prefix "temp/" after 1 day - BucketLifecycleConfiguration.Rule rule = new BucketLifecycleConfiguration.Rule() - .withId("abort-temp-uploads") - .withPrefix("temp/") - .withStatus(BucketLifecycleConfiguration.ENABLED); - rule.setAbortIncompleteMultipartUpload( - new AbortIncompleteMultipartUpload().withDaysAfterInitiation(1)); - - BucketLifecycleConfiguration config = new BucketLifecycleConfiguration(); - config.setRules(Collections.singletonList(rule)); - s3Client.setBucketLifecycleConfiguration(bucketName, config); - - // Trigger lifecycle service - lifecycleService.runPeriodicalTaskNow(); - - // Wait for abort - GenericTestUtils.waitFor(() -> { - MultipartUploadListing listing = s3Client.listMultipartUploads( - new ListMultipartUploadsRequest(bucketName)); - return listing.getMultipartUploads().size() == 2; - }, 500, 30000); - - // Verify results - MultipartUploadListing listAfter = s3Client.listMultipartUploads( - new ListMultipartUploadsRequest(bucketName)); - List remainingKeys = listAfter.getMultipartUploads().stream() - .map(MultipartUpload::getKey) - .collect(Collectors.toList()); - - assertFalse(remainingKeys.contains(matchingOldKey), "Old MPU with matching prefix should be aborted"); - assertTrue(remainingKeys.contains(nonMatchingOldKey), "Old MPU with non-matching prefix should remain"); - assertTrue(remainingKeys.contains(matchingRecentKey), "Recent MPU should remain"); - } - - private void updateMpuCreationTime(OMMetadataManager metadataManager, - String volumeName, String bucketName, String keyName, - String uploadId, long newCreationTime) throws Exception { - String multipartKey = metadataManager.getMultipartKey(volumeName, bucketName, keyName, uploadId); - OmMultipartKeyInfo existingInfo = metadataManager.getMultipartInfoTable().get(multipartKey); - if (existingInfo == null) { - throw new RuntimeException("Multipart key info not found: " + multipartKey); - } - - OmMultipartKeyInfo updatedInfo = new OmMultipartKeyInfo.Builder() - .setUploadID(existingInfo.getUploadID()) - .setCreationTime(newCreationTime) - .setReplicationConfig(existingInfo.getReplicationConfig()) - .setObjectID(existingInfo.getObjectID()) - .setUpdateID(existingInfo.getUpdateID()) - .setParentID(existingInfo.getParentID()) - .build(); - - metadataManager.getMultipartInfoTable().put(multipartKey, updatedInfo); - } - - @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - class PresignedUrlTests { - private static final String BUCKET_NAME = "v1-presigned-url-bucket"; - private static final String CONTENT = "bar"; - // Set the presigned URL to expire after one hour. - private final Date expiration = Date.from(Instant.now().plusMillis(1000 * 60 * 60)); - - @BeforeAll - public void setup() { - s3Client.createBucket(BUCKET_NAME); - } - - @Test - public void testPresignedUrlGet() throws IOException { - final String keyName = getKeyName(); - - InputStream is = new ByteArrayInputStream(CONTENT.getBytes(StandardCharsets.UTF_8)); - - s3Client.putObject(BUCKET_NAME, keyName, is, new ObjectMetadata()); - - // Generate the presigned URL - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(BUCKET_NAME, keyName).withMethod(HttpMethod.GET).withExpiration(expiration); - generatePresignedUrlRequest.addRequestParameter("x-custom-parameter", "custom-value"); - URL presignedUrl = s3Client.generatePresignedUrl(generatePresignedUrlRequest); - - // Download the object using HttpUrlConnection (since v1.1) - // Capture the response body to a byte array. - HttpURLConnection connection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "GET", null, null); - // Download the result of executing the request. - try (InputStream s3is = connection.getInputStream(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(CONTENT.getBytes(StandardCharsets.UTF_8).length)) { - IOUtils.copy(s3is, bos); - assertEquals(CONTENT, bos.toString("UTF-8")); - } - } - - @ParameterizedTest - @ValueSource(strings = {"annotation", "torrent"}) - public void testPresignedUrlGetObjectSubresourceNotImplemented(String subresource) throws Exception { - final String keyName = getKeyName(); - - InputStream is = new ByteArrayInputStream(CONTENT.getBytes(StandardCharsets.UTF_8)); - s3Client.putObject(BUCKET_NAME, keyName, is, new ObjectMetadata()); - - // AmazonS3 (SDK v1) has no getObjectTorrent or listObjectAnnotations API, so exercise the same HTTP behavior - // via a presigned URL with the subresource query parameter, as with other request shapes - // the typed v1 API doesn't expose. - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(BUCKET_NAME, keyName).withMethod(HttpMethod.GET).withExpiration(expiration); - generatePresignedUrlRequest.addRequestParameter(subresource, ""); - URL presignedUrl = s3Client.generatePresignedUrl(generatePresignedUrlRequest); - - HttpURLConnection connection = null; - try { - connection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "GET", null, null); - assertEquals(HttpURLConnection.HTTP_NOT_IMPLEMENTED, connection.getResponseCode()); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - - // object must be untouched - ObjectMetadata metadata = s3Client.getObjectMetadata(BUCKET_NAME, keyName); - assertEquals(CONTENT.length(), metadata.getContentLength()); - } - - @Test - public void testPresignedUrlHead() throws IOException { - final String keyName = getKeyName(); - - InputStream is = new ByteArrayInputStream(CONTENT.getBytes(StandardCharsets.UTF_8)); - s3Client.putObject(BUCKET_NAME, keyName, is, new ObjectMetadata()); - - // Test HeadObject presigned URL - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(BUCKET_NAME, keyName).withMethod(HttpMethod.HEAD).withExpiration(expiration); - URL presignedUrl = s3Client.generatePresignedUrl(generatePresignedUrlRequest); - - HttpURLConnection connection = null; - try { - connection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "HEAD", null, null); - int responseCode = connection.getResponseCode(); - assertEquals(200, responseCode, "HeadObject presigned URL should return 200 OK"); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - - // Test HeadBucket presigned URL - GeneratePresignedUrlRequest generateBucketPresignedUrlRequest = - new GeneratePresignedUrlRequest(BUCKET_NAME, null).withMethod(HttpMethod.HEAD).withExpiration(expiration); - URL presignedBucketUrl = s3Client.generatePresignedUrl(generateBucketPresignedUrlRequest); - - HttpURLConnection bucketConnection = null; - try { - bucketConnection = S3SDKTestUtils.openHttpURLConnection(presignedBucketUrl, "HEAD", null, null); - int bucketResponseCode = bucketConnection.getResponseCode(); - assertEquals(200, bucketResponseCode, "HeadBucket presigned URL should return 200 OK"); - } finally { - if (bucketConnection != null) { - bucketConnection.disconnect(); - } - } - } - - @Test - public void testPresignedUrlPutObject() throws Exception { - final String keyName = getKeyName(); - - // Test PutObjectRequest presigned URL - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(BUCKET_NAME, keyName).withMethod(HttpMethod.PUT).withExpiration(expiration); - URL presignedUrl = s3Client.generatePresignedUrl(generatePresignedUrlRequest); - - HttpURLConnection connection = null; - try { - connection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "PUT", - null, CONTENT.getBytes(StandardCharsets.UTF_8)); - int responseCode = connection.getResponseCode(); - assertEquals(200, responseCode, "PutObject presigned URL should return 200 OK"); - String actualContent; - S3Object s3Object = s3Client.getObject(BUCKET_NAME, keyName); - try (S3ObjectInputStream inputStream = s3Object.getObjectContent()) { - actualContent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - } - assertEquals(CONTENT, actualContent, "Downloaded content should match uploaded content"); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - } - - @Test - public void testPresignedUrlPutSingleChunkWithWrongSha256() throws Exception { - final String keyName = getKeyName(); - - // Test PutObjectRequest presigned URL - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(BUCKET_NAME, keyName).withMethod(HttpMethod.PUT).withExpiration(expiration); - URL presignedUrl = s3Client.generatePresignedUrl(generatePresignedUrlRequest); - - Map> headers = new HashMap<>(); - List sha256Value = new ArrayList<>(); - sha256Value.add("wrong-sha256-value"); - headers.put("x-amz-content-sha256", sha256Value); - - HttpURLConnection connection = null; - try { - connection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "PUT", - headers, CONTENT.getBytes(StandardCharsets.UTF_8)); - int responseCode = connection.getResponseCode(); - assertEquals(400, responseCode, "PutObject presigned URL should return 400 because of wrong SHA256"); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - - // Verify the object was not uploaded - AmazonServiceException ase = assertThrows(AmazonServiceException.class, - () -> s3Client.getObject(BUCKET_NAME, keyName)); - - assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(404, ase.getStatusCode()); - assertEquals("NoSuchKey", ase.getErrorCode()); - } - - @Test - public void testPresignedUrlMultipartUpload(@TempDir Path tempDir) throws Exception { - final String keyName = getKeyName(); - final Map userMetadata = new HashMap<>(); - userMetadata.put("key1", "value1"); - userMetadata.put("key2", "value2"); - final Map tags = new HashMap<>(); - tags.put("tag1", "value1"); - tags.put("tag2", "value2"); - - File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); - createFile(multipartUploadFile, (int) (10 * MB)); - - // create MPU using presigned URL - GeneratePresignedUrlRequest initMPUPresignedUrlRequest = - createInitMPUPresignedUrlRequest(keyName, userMetadata, tags); - - - String uploadId = initMultipartUpload(initMPUPresignedUrlRequest); - - // upload parts using presigned URL - List completedParts = uploadParts(multipartUploadFile, keyName, uploadId); - - // Complete multipart upload using presigned URL - completeMPU(keyName, uploadId, completedParts); - - // Verify upload result - ObjectMetadata objectMeta = s3Client.getObjectMetadata(BUCKET_NAME, keyName); - assertEquals(userMetadata, objectMeta.getUserMetadata()); - - // Verify content - S3Object s3Object = s3Client.getObject(BUCKET_NAME, keyName); - assertEquals(tags.size(), s3Object.getTaggingCount()); - String actualContent; - try (S3ObjectInputStream inputStream = s3Object.getObjectContent()) { - actualContent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - } - String expectedContent = new String(Files.readAllBytes(multipartUploadFile.toPath()), StandardCharsets.UTF_8); - assertEquals(expectedContent, actualContent, "Downloaded content should match uploaded content"); - } - - private void completeMPU(String keyName, String uploadId, List completedParts) throws IOException { - GeneratePresignedUrlRequest request = - new GeneratePresignedUrlRequest(BUCKET_NAME, keyName).withMethod(HttpMethod.POST).withExpiration(expiration); - request.addRequestParameter("uploadId", uploadId); - - URL presignedUrl = s3Client.generatePresignedUrl(request); - - // Generate completion XML payload - StringBuilder completionXml = new StringBuilder(); - completionXml.append("\n"); - for (PartETag part : completedParts) { - completionXml.append(" \n") - .append(" ").append(part.getPartNumber()).append("\n") - .append(" ").append(stripQuotes(part.getETag())).append("\n") - .append(" \n"); - } - completionXml.append(""); - - byte[] completionPayloadBytes = completionXml.toString().getBytes(StandardCharsets.UTF_8); - - HttpURLConnection httpConnection = null; - try { - httpConnection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "POST", null, completionPayloadBytes); - int responseCode = httpConnection.getResponseCode(); - assertEquals(200, responseCode, "Complete multipart upload should return 200 OK"); - } finally { - if (httpConnection != null) { - httpConnection.disconnect(); - } - } - } - - private List uploadParts(File multipartUploadFile, String keyName, String uploadId) throws IOException { - List completedParts = new ArrayList<>(); - ByteBuffer byteBuffer = ByteBuffer.allocate((int) (5 * MB)); - long filePosition = 0; - long fileLength = multipartUploadFile.length(); - int partNumber = 1; - - try (RandomAccessFile file = new RandomAccessFile(multipartUploadFile, "r")) { - while (filePosition < fileLength) { - file.seek(filePosition); - long bytesRead = file.getChannel().read(byteBuffer); - byteBuffer.flip(); - - // generate presigned URL for each part - GeneratePresignedUrlRequest request = - new GeneratePresignedUrlRequest(BUCKET_NAME, keyName).withMethod(HttpMethod.PUT) - .withExpiration(expiration); - request.addRequestParameter("partNumber", String.valueOf(partNumber)); - request.addRequestParameter("uploadId", uploadId); - - URL presignedUrl = s3Client.generatePresignedUrl(request); - - // upload each part using presigned URL - HttpURLConnection connection = null; - try { - Map> headers = new HashMap<>(); - List header = Collections.singletonList(String.valueOf(byteBuffer.remaining())); - headers.put("Content-Length", header); - byte[] body = new byte[byteBuffer.remaining()]; - byteBuffer.get(body); - - connection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "PUT", headers, body); - int responseCode = connection.getResponseCode(); - assertEquals(200, responseCode, String.format("Upload part %d should return 200 OK", partNumber)); - - String etag = connection.getHeaderField("ETag"); - PartETag partETag = new PartETag(partNumber, etag); - completedParts.add(partETag); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - - byteBuffer.clear(); - filePosition += bytesRead; - partNumber++; - } - } - return completedParts; - } - - private String initMultipartUpload(GeneratePresignedUrlRequest request) throws IOException { - URL presignedUrl = s3Client.generatePresignedUrl(request); - String uploadId; - HttpURLConnection httpConnection = null; - try { - Map customRequestHeaders = request.getCustomRequestHeaders(); - Map> headers = new HashMap<>(); - if (customRequestHeaders != null) { - for (Map.Entry entry : customRequestHeaders.entrySet()) { - headers.put(entry.getKey(), Collections.singletonList(entry.getValue())); - } - } - httpConnection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "POST", headers, null); - int initMPUConnectionResponseCode = httpConnection.getResponseCode(); - assertEquals(200, initMPUConnectionResponseCode); - - try (InputStream is = httpConnection.getInputStream()) { - String responseXml = IOUtils.toString(is, StandardCharsets.UTF_8); - uploadId = S3SDKTestUtils.extractUploadId(responseXml); - } - } finally { - if (httpConnection != null) { - httpConnection.disconnect(); - } - } - return uploadId; - } - - private GeneratePresignedUrlRequest createInitMPUPresignedUrlRequest(String keyName, - Map userMetadata, - Map tags) throws Exception { - GeneratePresignedUrlRequest initMPUPresignUrlRequest = - new GeneratePresignedUrlRequest(BUCKET_NAME, keyName).withMethod(HttpMethod.POST).withExpiration(expiration); - - userMetadata.forEach((k, v) -> { - initMPUPresignUrlRequest.putCustomRequestHeader(CUSTOM_METADATA_HEADER_PREFIX + k, v); - }); - - StringBuilder tagValueBuilder = new StringBuilder(); - for (Map.Entry entry : tags.entrySet()) { - if (tagValueBuilder.length() > 0) { - tagValueBuilder.append('&'); - } - tagValueBuilder.append(entry.getKey()).append('=').append(URLEncoder.encode(entry.getValue(), "UTF-8")); - } - initMPUPresignUrlRequest.putCustomRequestHeader(S3Consts.TAG_HEADER, tagValueBuilder.toString()); - return initMPUPresignUrlRequest; - } - - @Test - public void testPresignedUrlDelete() throws IOException { - final String keyName = getKeyName(); - - try (InputStream is = new ByteArrayInputStream(CONTENT.getBytes(StandardCharsets.UTF_8))) { - s3Client.putObject(BUCKET_NAME, keyName, is, new ObjectMetadata()); - } - - // Generate the presigned URL for DELETE - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(BUCKET_NAME, keyName).withMethod(HttpMethod.DELETE) - .withExpiration(expiration); - URL url = s3Client.generatePresignedUrl(generatePresignedUrlRequest); - - // Execute the DELETE request using HttpUrlConnection - HttpURLConnection connection = null; - try { - connection = S3SDKTestUtils.openHttpURLConnection(url, "DELETE", null, null); - int responseCode = connection.getResponseCode(); - // Verify the response code is 204 (No Content) - assertEquals(HttpURLConnection.HTTP_NO_CONTENT, responseCode); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - - // Verify the object is deleted - assertFalse(s3Client.doesObjectExist(BUCKET_NAME, keyName)); - } - } - - /** - * Tests the functionality to create a snapshot of an Ozone bucket and then read files - * from the snapshot directory using the S3 SDK. - * - *

The test follows these steps: - *

    - *
  1. Create a bucket and upload a file via the S3 client.
  2. - *
  3. Create a snapshot on the bucket using the Ozone client.
  4. - *
  5. Construct the snapshot object key using the ".snapshot" directory format.
  6. - *
  7. Retrieve the object from the snapshot and verify that its content matches - * the originally uploaded content.
  8. - *
- *

- * - * @throws Exception if the test fails due to any errors during bucket creation, snapshot creation, - * file upload, or retrieval. - */ - @Test - public void testReadSnapshotDirectoryUsingS3SDK() throws Exception { - final String bucketName = getBucketName("snapshot"); - final String keyName = getKeyName("snapshotfile"); - final String content = "snapshot test content"; - final byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); - - // Create the bucket and upload an object using S3 SDK. - InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - s3Client.createBucket(bucketName); - s3Client.putObject(bucketName, keyName, - is, new ObjectMetadata()); - - String snapshotName = "snap1"; - // Create a snapshot using the Ozone client. - // Snapshots in Ozone are created on the bucket and are exposed via the ".snapshot" directory. - try (OzoneClient ozoneClient = cluster.newClient()) { - ObjectStore store = ozoneClient.getObjectStore(); - OzoneVolume volume = store.getS3Volume(); - - store.createSnapshot(volume.getName(), bucketName, snapshotName); - } - - // Use the S3 SDK to read the file from the snapshot directory. - // The key in the snapshot is constructed using the special ".snapshot" prefix. - String snapshotKey = ".snapshot/" + snapshotName + "/" + keyName; - - S3Object s3Object = s3Client.getObject(bucketName, snapshotKey); - try (S3ObjectInputStream s3is = s3Object.getObjectContent(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(contentBytes.length)) { - byte[] readBuf = new byte[1024]; - int readLen = 0; - while ((readLen = s3is.read(readBuf)) > 0) { - bos.write(readBuf, 0, readLen); - } - assertEquals(content, bos.toString("UTF-8")); - } - } - - private boolean isBucketEmpty(Bucket bucket) { - ObjectListing objectListing = s3Client.listObjects(bucket.getName()); - return objectListing.getObjectSummaries().isEmpty(); - } - - private String getBucketName() { - return getBucketName(""); - } - - private String getBucketName(String ignored) { - return uniqueObjectName(); - } - - private String getKeyName() { - return getKeyName(""); - } - - private String getKeyName(String ignored) { - return uniqueObjectName(); - } - - private static void assertNotImplemented(Executable request) { - final AmazonServiceException ase = assertThrows(AmazonServiceException.class, request); - assertEquals(S3ErrorTable.NOT_IMPLEMENTED.getCode(), ase.getErrorCode()); - assertEquals(501, ase.getStatusCode()); - assertEquals(ErrorType.Service, ase.getErrorType()); - } - - private String multipartUpload(String bucketName, String key, File file, long partSize, String contentType, - Map userMetadata, List tags) throws Exception { - String uploadId = initiateMultipartUpload(bucketName, key, contentType, userMetadata, tags); - - List partETags = uploadParts(bucketName, key, uploadId, file, partSize); - - completeMultipartUpload(bucketName, key, uploadId, partETags); - - return uploadId; - } - - private String initiateMultipartUpload(String bucketName, String key, String contentType, - Map metadata, List tags) { - InitiateMultipartUploadRequest initRequest; - if (metadata == null || metadata.isEmpty()) { - initRequest = new InitiateMultipartUploadRequest(bucketName, key); - } else { - ObjectMetadata objectMetadata = new ObjectMetadata(); - objectMetadata.setUserMetadata(metadata); - if (contentType != null) { - objectMetadata.setContentType(contentType); - } - - initRequest = new InitiateMultipartUploadRequest(bucketName, key, objectMetadata) - .withTagging(new ObjectTagging(tags)); - } - - InitiateMultipartUploadResult initResponse = s3Client.initiateMultipartUpload(initRequest); - assertEquals(bucketName, initResponse.getBucketName()); - assertEquals(key, initResponse.getKey()); - // TODO: Once bucket lifecycle configuration is supported, should check for "abortDate" and "abortRuleId" - - return initResponse.getUploadId(); - } - - // TODO: Also support async upload parts (similar to v2 asyncClient) - private List uploadParts(String bucketName, String key, String uploadId, File file, long partSize) - throws Exception { - // Create a list of ETag objects. You retrieve ETags for each object part - // uploaded, - // then, after each individual part has been uploaded, pass the list of ETags to - // the request to complete the upload. - List partETags = new ArrayList<>(); - - // Upload the file parts. - long filePosition = 0; - long fileLength = file.length(); - try (InputStream fileInputStream = Files.newInputStream(file.toPath())) { - for (int i = 1; filePosition < fileLength; i++) { - // Because the last part could be less than 5 MB, adjust the part size as - // needed. - partSize = Math.min(partSize, (fileLength - filePosition)); - - // Create the request to upload a part. - UploadPartRequest uploadRequest = new UploadPartRequest() - .withBucketName(bucketName) - .withKey(key) - .withUploadId(uploadId) - .withPartNumber(i) - .withFileOffset(filePosition) - .withFile(file) - .withPartSize(partSize); - - // Upload the part and add the response's ETag to our list. - UploadPartResult uploadResult = s3Client.uploadPart(uploadRequest); - PartETag partETag = uploadResult.getPartETag(); - assertEquals(i, partETag.getPartNumber()); - assertEquals(DatatypeConverter.printHexBinary( - calculateDigest(fileInputStream, 0, (int) partSize)).toLowerCase(), partETag.getETag()); - partETags.add(partETag); - - filePosition += partSize; - } - } - - return partETags; - } - - private void completeMultipartUpload(String bucketName, String key, String uploadId, List partETags) { - CompleteMultipartUploadRequest compRequest = new CompleteMultipartUploadRequest(bucketName, key, - uploadId, partETags); - CompleteMultipartUploadResult compResponse = s3Client.completeMultipartUpload(compRequest); - assertEquals(bucketName, compResponse.getBucketName()); - assertEquals(key, compResponse.getKey()); - } - - private void abortMultipartUpload(String bucketName, String key, String uploadId) { - AbortMultipartUploadRequest abortRequest = new AbortMultipartUploadRequest(bucketName, key, uploadId); - s3Client.abortMultipartUpload(abortRequest); - } -} diff --git a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java index d42284b08b0a..a3938fe91c34 100644 --- a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java +++ b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java @@ -63,18 +63,22 @@ import java.util.Base64; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import javax.xml.bind.DatatypeConverter; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.hadoop.hdds.client.DefaultReplicationConfig; import org.apache.hadoop.hdds.client.ECReplicationConfig; +import org.apache.hadoop.hdds.client.OzoneQuota; import org.apache.hadoop.hdds.client.ReplicationConfig; import org.apache.hadoop.hdds.client.ReplicationFactor; import org.apache.hadoop.hdds.client.ReplicationType; @@ -169,6 +173,8 @@ import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.GetObjectTaggingRequest; import software.amazon.awssdk.services.s3.model.GetObjectTaggingResponse; +import software.amazon.awssdk.services.s3.model.Grant; +import software.amazon.awssdk.services.s3.model.Grantee; import software.amazon.awssdk.services.s3.model.HeadBucketRequest; import software.amazon.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.awssdk.services.s3.model.HeadObjectResponse; @@ -188,12 +194,19 @@ import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.ListPartsRequest; +import software.amazon.awssdk.services.s3.model.ListPartsResponse; import software.amazon.awssdk.services.s3.model.MetadataDirective; +import software.amazon.awssdk.services.s3.model.MultipartUpload; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.services.s3.model.NoSuchUploadException; import software.amazon.awssdk.services.s3.model.ObjectAttributes; import software.amazon.awssdk.services.s3.model.ObjectIdentifier; import software.amazon.awssdk.services.s3.model.ObjectLockLegalHoldStatus; import software.amazon.awssdk.services.s3.model.ObjectVersion; +import software.amazon.awssdk.services.s3.model.Owner; +import software.amazon.awssdk.services.s3.model.Part; +import software.amazon.awssdk.services.s3.model.Permission; import software.amazon.awssdk.services.s3.model.PutBucketAclRequest; import software.amazon.awssdk.services.s3.model.PutBucketTaggingRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -203,6 +216,7 @@ import software.amazon.awssdk.services.s3.model.S3Object; import software.amazon.awssdk.services.s3.model.Tag; import software.amazon.awssdk.services.s3.model.Tagging; +import software.amazon.awssdk.services.s3.model.Type; import software.amazon.awssdk.services.s3.model.UploadPartCopyRequest; import software.amazon.awssdk.services.s3.model.UploadPartCopyResponse; import software.amazon.awssdk.services.s3.model.UploadPartRequest; @@ -225,6 +239,7 @@ import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; import software.amazon.awssdk.services.s3.presigner.model.UploadPartPresignRequest; import software.amazon.awssdk.transfer.s3.S3TransferManager; +import software.amazon.awssdk.transfer.s3.model.CompletedFileUpload; import software.amazon.awssdk.transfer.s3.model.DownloadFileRequest; import software.amazon.awssdk.transfer.s3.model.FileDownload; import software.amazon.awssdk.transfer.s3.model.ResumableFileDownload; @@ -254,6 +269,7 @@ public abstract class AbstractS3SDKV2Tests extends OzoneTestBase implements NonH */ public static final String EC_PARALLEL_READ_S3G_HTTP_ADDRESS_KEY = "ozone.test.s3g.ec.parallel.read.http.address"; + private static final int MAX_UPLOADS_LIMIT = 1000; private static final String READ_CONSISTENCY_HEADER = "x-ozone-read-consistency"; private static final String LOCAL_LEASE_LOG_LIMIT_HEADER = @@ -314,6 +330,189 @@ public void testCreateBucketAlreadyExistsDifferentOwner() throws IOException { exception.awsErrorDetails().errorCode()); } + @Test + public void testCreateBucket() { + final String bucketName = getBucketName(); + + s3Client.createBucket(b -> b.bucket(bucketName)); + + s3Client.headBucket(b -> b.bucket(bucketName)); + s3Client.getBucketAcl(b -> b.bucket(bucketName)); + assertThat(s3Client.listObjectsV2(b -> b.bucket(bucketName)).contents()).isEmpty(); + } + + @Test + public void testBucketAclOperations() { + // TODO HDDS-11738: Assert the bucket ACL when bucket S3 ACL logic has been fixed + final String bucketName = getBucketName(); + final Owner owner = Owner.builder().id("owner").displayName("owner").build(); + final Grantee grantee = Grantee.builder().type(Type.CANONICAL_USER).id("testGrantee").build(); + + s3Client.createBucket(b -> b.bucket(bucketName).grantRead("id=testGrantee")); + + s3Client.putBucketAcl(b -> b.bucket(bucketName).accessControlPolicy(p -> p.owner(owner).grants( + Grant.builder().grantee(grantee).permission(Permission.READ).build(), + Grant.builder().grantee(grantee).permission(Permission.WRITE).build()))); + } + + @Test + public void testDeleteBucket() { + final String bucketName = getBucketName(); + s3Client.createBucket(b -> b.bucket(bucketName)); + + s3Client.deleteBucket(b -> b.bucket(bucketName)); + + final S3Exception exception = assertThrows(S3Exception.class, () -> s3Client.headBucket(b -> b.bucket(bucketName))); + assertEquals(404, exception.statusCode()); + } + + @Test + public void testDeleteBucketNotExist() { + final String bucketName = getBucketName(); + + final S3Exception exception = assertThrows(S3Exception.class, + () -> s3Client.deleteBucket(b -> b.bucket(bucketName))); + assertEquals(404, exception.statusCode()); + assertEquals(S3ErrorTable.NO_SUCH_BUCKET.getCode(), exception.awsErrorDetails().errorCode()); + } + + @Test + public void testDeleteBucketNonEmptyWithKeys() { + final String bucketName = getBucketName(); + s3Client.createBucket(b -> b.bucket(bucketName)); + for (int i = 1; i <= 10; i++) { + final String keyName = "key-" + i; + s3Client.putObject(b -> b.bucket(bucketName).key(keyName), + RequestBody.fromString(RandomStringUtils.secure().nextAlphanumeric(1024))); + } + + // Bucket deletion should fail if there are still keys in the bucket + final S3Exception exception = assertThrows(S3Exception.class, + () -> s3Client.deleteBucket(b -> b.bucket(bucketName))); + assertEquals(409, exception.statusCode()); + assertEquals(S3ErrorTable.BUCKET_NOT_EMPTY.getCode(), exception.awsErrorDetails().errorCode()); + + for (S3Object object : s3Client.listObjectsV2Paginator(b -> b.bucket(bucketName)).contents()) { + s3Client.deleteObject(b -> b.bucket(bucketName).key(object.key())); + } + } + + @Test + public void testDeleteBucketNonEmptyWithIncompleteMultipartUpload(@TempDir Path tempDir) throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(b -> b.bucket(bucketName)); + final File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); + createFile(multipartUploadFile, (int) (5 * MB)); + + // An incomplete multipart upload: parts uploaded, but not completed + final String uploadId = initiateMultipartUpload(bucketName, keyName, new HashMap<>(), Collections.emptyList()); + uploadParts(bucketName, keyName, uploadId, multipartUploadFile, (int) MB); + + final S3Exception exception = assertThrows(S3Exception.class, + () -> s3Client.deleteBucket(b -> b.bucket(bucketName))); + assertEquals(409, exception.statusCode()); + assertEquals(S3ErrorTable.BUCKET_NOT_EMPTY.getCode(), exception.awsErrorDetails().errorCode()); + + // After the multipart upload is aborted, the bucket deletion should succeed + s3Client.abortMultipartUpload(b -> b.bucket(bucketName).key(keyName).uploadId(uploadId)); + s3Client.deleteBucket(b -> b.bucket(bucketName)); + assertThrows(NoSuchBucketException.class, () -> s3Client.headBucket(b -> b.bucket(bucketName))); + } + + @Test + public void testQuotaExceeded() throws IOException { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(b -> b.bucket(bucketName)); + try (OzoneClient ozoneClient = cluster.newClient()) { + ozoneClient.getObjectStore().getS3Volume().getBucket(bucketName).setQuota(OzoneQuota.parseQuota("1", "10")); + } + + final String content = RandomStringUtils.secure().nextAlphanumeric(1024); + final S3Exception exception = assertThrows(S3Exception.class, + () -> s3Client.putObject(b -> b.bucket(bucketName).key(keyName), RequestBody.fromString(content))); + assertEquals(403, exception.statusCode()); + assertEquals(S3ErrorTable.QUOTA_EXCEEDED.getCode(), exception.awsErrorDetails().errorCode()); + } + + @Test + public void testGetObject() { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + final String content = "bar"; + final Map userMetadata = new HashMap<>(); + userMetadata.put("key1", "value1"); + userMetadata.put("key2", "value2"); + s3Client.createBucket(b -> b.bucket(bucketName)); + + s3Client.putObject(b -> b.bucket(bucketName).key(keyName).metadata(userMetadata) + .tagging("tag1=value1&tag2=value2"), RequestBody.fromString(content)); + + final ResponseBytes object = s3Client.getObjectAsBytes(b -> b.bucket(bucketName).key(keyName)); + assertEquals(2, object.response().tagCount()); + assertEquals(userMetadata, object.response().metadata()); + assertEquals(content, object.asUtf8String()); + } + + @Test + public void testGetObjectWithoutETag() throws Exception { + // Object uploaded using other protocols (e.g. ofs / ozone cli) will not have ETag. + // Ensure that GetObject does not validate the ETag if there is none. + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + final String value = "sample value"; + final byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8); + s3Client.createBucket(b -> b.bucket(bucketName)); + try (OzoneClient ozoneClient = cluster.newClient()) { + final OzoneBucket bucket = ozoneClient.getObjectStore().getS3Volume().getBucket(bucketName); + try (OzoneOutputStream out = bucket.createKey(keyName, valueBytes.length, + ReplicationConfig.fromTypeAndFactor(ReplicationType.RATIS, ReplicationFactor.ONE), Collections.emptyMap())) { + out.write(valueBytes); + } + } + + final ResponseBytes object = s3Client.getObjectAsBytes(b -> b.bucket(bucketName).key(keyName)); + assertNull(object.response().eTag()); + assertEquals(value, object.asUtf8String()); + } + + @Test + public void testPutDoubleSlashPrefixObject() throws Exception { + final String bucketName = getBucketName(); + final String keyName = "//dir1"; + final String content = "bar"; + createFsoBucket(bucketName); + + final PutObjectResponse putObjectResponse = s3Client.putObject(b -> b.bucket(bucketName).key(keyName), + RequestBody.fromString(content)); + assertEquals("\"37b51d194a7513e45b56f6524f2d51f2\"", putObjectResponse.eTag()); + + final ResponseBytes object = s3Client.getObjectAsBytes(b -> b.bucket(bucketName).key(keyName)); + assertEquals(content.length(), object.response().contentLength()); + assertEquals(content, object.asUtf8String()); + } + + @Test + public void testPutObjectAcl() { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + final String content = "bar"; + s3Client.createBucket(b -> b.bucket(bucketName)); + final String originalETag = s3Client.putObject(b -> b.bucket(bucketName).key(keyName), + RequestBody.fromString(content)).eTag(); + final Owner owner = Owner.builder().id("owner").displayName("owner").build(); + final Grantee grantee = Grantee.builder().type(Type.CANONICAL_USER).id("testGrantee").build(); + + assertNotImplemented(() -> s3Client.putObjectAcl(b -> b.bucket(bucketName).key(keyName) + .accessControlPolicy(p -> p.owner(owner) + .grants(Grant.builder().grantee(grantee).permission(Permission.READ).build())))); + + // Ensure that the object remains unchanged + assertEquals(originalETag, s3Client.headObject(b -> b.bucket(bucketName).key(keyName)).eTag()); + assertEquals(content, s3Client.getObjectAsBytes(b -> b.bucket(bucketName).key(keyName)).asUtf8String()); + } + @Test public void testPutObject() { final String bucketName = getBucketName(); @@ -2960,6 +3159,27 @@ public void tearDown() { sdkHttpClient.close(); } + @Test + public void testPresignedUrlGetWithCustomQueryParameter() throws Exception { + final String keyName = getKeyName(); + s3Client.putObject(b -> b.bucket(BUCKET_NAME).key(keyName), RequestBody.fromString(CONTENT)); + + // a query parameter unknown to S3, signed with the request + final GetObjectRequest objectRequest = GetObjectRequest.builder().bucket(BUCKET_NAME).key(keyName) + .overrideConfiguration(o -> o.putRawQueryParameter("x-custom-parameter", "custom-value")) + .build(); + final URL presignedUrl = presigner.presignGetObject(b -> b.signatureDuration(duration) + .getObjectRequest(objectRequest)).url(); + assertThat(presignedUrl.getQuery()).contains("x-custom-parameter=custom-value"); + + final HttpURLConnection connection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "GET", null, null); + try (InputStream s3is = connection.getInputStream()) { + assertEquals(CONTENT, IoUtils.toUtf8String(s3is)); + } finally { + connection.disconnect(); + } + } + @Test public void testPresignedUrlGet() throws Exception { final String keyName = getKeyName(); @@ -3958,6 +4178,267 @@ private void updateMpuCreationTime(OMMetadataManager metadataManager, metadataManager.getMultipartInfoTable().put(multipartKey, updatedInfo); } + @Test + public void testListObjectsSpecialKeyNames() { + final String bucketName = getBucketName("special-keys"); + final String content = "x"; + s3Client.createBucket(b -> b.bucket(bucketName)); + for (String keyName : S3SDKTestUtils.S3_SPECIAL_KEY_NAMES) { + s3Client.putObject(b -> b.bucket(bucketName).key(keyName), RequestBody.fromString(content)); + } + + // ListObjects (the original API, not ListObjectsV2) + final ListObjectsResponse response = s3Client.listObjects(b -> b.bucket(bucketName)); + assertEquals(S3SDKTestUtils.S3_SPECIAL_KEY_NAMES, + response.contents().stream().map(S3Object::key).collect(Collectors.toList())); + } + + @Test + public void testListObjectsBucketNotExist() { + final String bucketName = getBucketName(); + + final S3Exception exception = assertThrows(S3Exception.class, + () -> s3Client.listObjects(b -> b.bucket(bucketName))); + assertEquals(404, exception.statusCode()); + assertEquals(S3ErrorTable.NO_SUCH_BUCKET.getCode(), exception.awsErrorDetails().errorCode()); + } + + @Test + public void testListObjectsV2BucketNotExist() { + final String bucketName = getBucketName(); + + final S3Exception exception = assertThrows(S3Exception.class, + () -> s3Client.listObjectsV2(b -> b.bucket(bucketName))); + assertEquals(404, exception.statusCode()); + assertEquals(S3ErrorTable.NO_SUCH_BUCKET.getCode(), exception.awsErrorDetails().errorCode()); + } + + @Test + public void testHighLevelMultipartUpload(@TempDir Path tempDir) throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + final long fileSize = 20 * MB; + final long partSize = 5 * MB; + s3Client.createBucket(b -> b.bucket(bucketName)); + final File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); + createFile(multipartUploadFile, (int) fileSize); + + // the transfer manager uploads in parallel parts when its client has multipart upload enabled + try (S3AsyncClient multipartClient = S3AsyncClient.builder() + .multipartEnabled(true) + .multipartConfiguration(c -> c.thresholdInBytes(partSize).minimumPartSizeInBytes(partSize)) + .forcePathStyle(true) + .disableS3ExpressSessionAuth(true) + .endpointOverride(s3Client.serviceClientConfiguration().endpointOverride().get()) + .region(s3Client.serviceClientConfiguration().region()) + .credentialsProvider(s3Client.serviceClientConfiguration().credentialsProvider()) + .build(); + S3TransferManager transferManager = S3TransferManager.builder().s3Client(multipartClient).build()) { + final CompletedFileUpload upload = transferManager.uploadFile(b -> b + .putObjectRequest(r -> r.bucket(bucketName).key(keyName)) + .source(multipartUploadFile)).completionFuture().get(); + + // a multipart upload ETag ends with the number of parts + assertThat(upload.response().eTag()).endsWith("-" + fileSize / partSize + "\""); + } + assertEquals(fileSize, s3Client.headObject(b -> b.bucket(bucketName).key(keyName)).contentLength()); + } + + @Test + public void testListMultipartUploads() { + final String bucketName = getBucketName(); + final String multipartKey1 = getKeyName("multipart1"); + final String multipartKey2 = getKeyName("multipart2"); + s3Client.createBucket(b -> b.bucket(bucketName)); + + final List uploadIds = new ArrayList<>(); + uploadIds.add(initiateMultipartUpload(bucketName, multipartKey1, new HashMap<>(), Collections.emptyList())); + uploadIds.add(initiateMultipartUpload(bucketName, multipartKey1, new HashMap<>(), Collections.emptyList())); + uploadIds.add(initiateMultipartUpload(bucketName, multipartKey2, new HashMap<>(), Collections.emptyList())); + + final ListMultipartUploadsResponse response = s3Client.listMultipartUploads(b -> b + .bucket(bucketName).maxUploads(5000)); + assertEquals(uploadIds, response.uploads().stream().map(MultipartUpload::uploadId).collect(Collectors.toList())); + } + + @ParameterizedTest + @ValueSource(ints = {10, 5000}) + public void testListMultipartUploadsPagination(int requestedMaxUploads) { + final String bucketName = getBucketName() + "-" + requestedMaxUploads; + final String multipartKeyPrefix = getKeyName("multipart"); + s3Client.createBucket(b -> b.bucket(bucketName)); + + final List allKeys = new ArrayList<>(); + final Map keyToUploadId = new HashMap<>(); + final int effectiveMaxUploads = Math.min(requestedMaxUploads, MAX_UPLOADS_LIMIT); + final int uploadsCreated = 2 * effectiveMaxUploads + 5; + final int expectedPages = uploadsCreated / effectiveMaxUploads + 1; + for (int i = 0; i < uploadsCreated; i++) { + final String key = String.format("%s-%04d", multipartKeyPrefix, i); + allKeys.add(key); + keyToUploadId.put(key, initiateMultipartUpload(bucketName, key, new HashMap<>(), Collections.emptyList())); + } + Collections.sort(allKeys); + + final Set retrievedKeys = new HashSet<>(); + String keyMarker = null; + String uploadIdMarker = null; + boolean truncated; + int pageCount = 0; + do { + final String requestKeyMarker = keyMarker; + final String requestUploadIdMarker = uploadIdMarker; + final ListMultipartUploadsResponse result = s3Client.listMultipartUploads(b -> b.bucket(bucketName) + .maxUploads(requestedMaxUploads).keyMarker(requestKeyMarker).uploadIdMarker(requestUploadIdMarker)); + pageCount++; + + if (pageCount < expectedPages) { + assertEquals(effectiveMaxUploads, result.uploads().size()); + assertTrue(result.isTruncated()); + } else { + assertEquals(uploadsCreated % effectiveMaxUploads, result.uploads().size()); + assertFalse(result.isTruncated()); + } + for (MultipartUpload upload : result.uploads()) { + retrievedKeys.add(upload.key()); + assertEquals(keyToUploadId.get(upload.key()), upload.uploadId()); + } + + assertThat(result.prefix()).isNullOrEmpty(); + assertEquals(requestUploadIdMarker, emptyToNull(result.uploadIdMarker())); + assertEquals(requestKeyMarker, emptyToNull(result.keyMarker())); + assertEquals(effectiveMaxUploads, result.maxUploads()); + if (result.isTruncated()) { + final MultipartUpload lastUploadOnPage = result.uploads().get(result.uploads().size() - 1); + assertEquals(lastUploadOnPage.key(), result.nextKeyMarker()); + assertEquals(lastUploadOnPage.uploadId(), result.nextUploadIdMarker()); + } else { + assertNull(emptyToNull(result.nextKeyMarker())); + assertNull(emptyToNull(result.nextUploadIdMarker())); + } + + keyMarker = result.nextKeyMarker(); + uploadIdMarker = result.nextUploadIdMarker(); + truncated = result.isTruncated(); + } while (truncated); + + assertEquals(expectedPages, pageCount); + assertEquals(uploadsCreated, retrievedKeys.size(), "Should retrieve all uploads"); + assertEquals(allKeys, retrievedKeys.stream().sorted().collect(Collectors.toList()), + "Retrieved keys should match expected keys in order"); + + final String prefix = multipartKeyPrefix + "-001"; + final ListMultipartUploadsResponse prefixResult = s3Client.listMultipartUploads(b -> b + .bucket(bucketName).prefix(prefix)); + assertEquals(prefix, prefixResult.prefix()); + assertEquals(IntStream.rangeClosed(0, 9).mapToObj(i -> prefix + i).collect(Collectors.toList()), + prefixResult.uploads().stream().map(MultipartUpload::key).collect(Collectors.toList())); + } + + @Test + public void testListMultipartUploadsPaginationCornerCases() { + final String bucketName = getBucketName(); + final String keyA = getKeyName("samekey"); + final String keyB = getKeyName("after"); + s3Client.createBucket(b -> b.bucket(bucketName)); + + // Several uploads for the same key, to verify the upload-id-marker semantics + final List keyAUploadIds = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + keyAUploadIds.add(initiateMultipartUpload(bucketName, keyA, new HashMap<>(), Collections.emptyList())); + } + // Another key, to ensure that the listing proceeds past keyA + initiateMultipartUpload(bucketName, keyB, new HashMap<>(), Collections.emptyList()); + // Uploads of the same key are listed in upload id order + Collections.sort(keyAUploadIds); + + // upload-id-marker at the lowest upload id: only the larger upload ids of keyA are returned + final List sameKeyIds = s3Client.listMultipartUploads(b -> b.bucket(bucketName) + .keyMarker(keyA).uploadIdMarker(keyAUploadIds.get(0)).maxUploads(100)) + .uploads().stream().filter(u -> keyA.equals(u.key())).map(MultipartUpload::uploadId) + .collect(Collectors.toList()); + assertThat(sameKeyIds).allSatisfy(id -> assertThat(id).isGreaterThan(keyAUploadIds.get(0))); + + // upload-id-marker at the highest upload id: no upload of keyA is returned + assertThat(s3Client.listMultipartUploads(b -> b.bucket(bucketName) + .keyMarker(keyA).uploadIdMarker(keyAUploadIds.get(2)).maxUploads(100)).uploads()) + .noneMatch(u -> keyA.equals(u.key())); + } + + @Test + public void testListParts(@TempDir Path tempDir) throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + final int maxParts = 2; + s3Client.createBucket(b -> b.bucket(bucketName)); + final String uploadId = initiateMultipartUpload(bucketName, keyName, new HashMap<>(), Collections.emptyList()); + final File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); + createFile(multipartUploadFile, (int) (5 * MB)); + final List uploadedParts = uploadParts(bucketName, keyName, uploadId, multipartUploadFile, + (int) MB); + + final List listedParts = new ArrayList<>(); + int partNumberMarker = 0; + int remainingParts = uploadedParts.size(); + ListPartsResponse response; + do { + final int marker = partNumberMarker; + response = s3Client.listParts(b -> b.bucket(bucketName).key(keyName).uploadId(uploadId) + .maxParts(maxParts).partNumberMarker(marker)); + if (remainingParts > maxParts) { + assertTrue(response.isTruncated()); + partNumberMarker = response.nextPartNumberMarker(); + remainingParts -= maxParts; + } else { + assertFalse(response.isTruncated()); + } + listedParts.addAll(response.parts()); + } while (response.isTruncated()); + + assertEquals(uploadedParts.size(), listedParts.size()); + for (int i = 0; i < uploadedParts.size(); i++) { + assertEquals(uploadedParts.get(i).partNumber(), listedParts.get(i).partNumber()); + assertEquals(uploadedParts.get(i).eTag(), stripQuotes(listedParts.get(i).eTag())); + } + } + + @Test + public void testListPartsNotFound() { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(b -> b.bucket(bucketName)); + + final S3Exception exception = assertThrows(NoSuchUploadException.class, + () -> s3Client.listParts(b -> b.bucket(bucketName).key(keyName).uploadId("nonexist"))); + assertEquals(404, exception.statusCode()); + } + + @Test + public void testGetParticularPart(@TempDir Path tempDir) throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(b -> b.bucket(bucketName)); + final File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); + createFile(multipartUploadFile, (int) (15 * MB)); + multipartUpload(bucketName, keyName, multipartUploadFile, (int) (5 * MB), new HashMap<>(), Collections.emptyList()); + + final long allPartsLength = s3Client.getObjectAsBytes(b -> b.bucket(bucketName).key(keyName)) + .response().contentLength(); + long partsLength = 0; + for (int part = 1; part <= 3; part++) { + final int partNumber = part; + final long partLength = s3Client.getObjectAsBytes(b -> b.bucket(bucketName).key(keyName).partNumber(partNumber)) + .response().contentLength(); + assertEquals(allPartsLength / 3, partLength, "part " + partNumber); + partsLength += partLength; + } + assertEquals(allPartsLength, partsLength); + } + + private static String emptyToNull(String value) { + return value == null || value.isEmpty() ? null : value; + } + private String getBucketName() { return getBucketName(""); } diff --git a/hadoop-ozone/integration-test-s3/src/test/resources/log4j.properties b/hadoop-ozone/integration-test-s3/src/test/resources/log4j.properties index 977e356b7ad3..27935677a2fa 100644 --- a/hadoop-ozone/integration-test-s3/src/test/resources/log4j.properties +++ b/hadoop-ozone/integration-test-s3/src/test/resources/log4j.properties @@ -23,8 +23,5 @@ log4j.logger.org.apache.hadoop.hdds.utils.db.managed=TRACE log4j.logger.org.apache.hadoop.hdds.utils.db.CodecBuffer=DEBUG log4j.logger.org.apache.hadoop.ozone.client.OzoneClientFactory=DEBUG -log4j.logger.com.amazonaws=WARN -log4j.logger.com.amazonaws.request=DEBUG - log4j.logger.software.amazon.awssdk=WARN log4j.logger.software.amazon.awssdk.request=DEBUG diff --git a/pom.xml b/pom.xml index a5d5c345c115..dcd5ca8bddee 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,6 @@ 1.9.25.1 1.9.7 3.27.7 - 1.12.788 2.55.6 0.8.0.RELEASE 1.86 @@ -363,23 +362,6 @@
- - com.amazonaws - aws-java-sdk-core - ${aws-java-sdk.version} - - - - commons-logging - commons-logging - - - - - com.amazonaws - aws-java-sdk-s3 - ${aws-java-sdk.version} - com.bettercloud vault-java-driver @@ -2121,6 +2103,23 @@ + + ban-aws-sdk-v1 + + enforce + + validate + + + + + com.amazonaws + + AWS SDK for Java v1 is end of life, use AWS SDK for Java v2 (software.amazon.awssdk) + + + + ban-imports @@ -2194,6 +2193,17 @@ ${project.build.directory}/generated-sources/protobuf/java + + true + AWS SDK for Java v1 is end of life, use AWS SDK for Java v2 (software.amazon.awssdk) + + com.amazonaws.** + + + ${project.build.directory}/generated-sources/java + ${project.build.directory}/generated-sources/protobuf/java + + true true From 19aac8dbfae8090a1172e59c9ce2e4bca996fca3 Mon Sep 17 00:00:00 2001 From: Andrey Yarovoy Date: Wed, 4 Nov 2026 22:34:12 -0500 Subject: [PATCH 2/3] Addressed review comments --- .../dist/src/main/smoketest/s3/freon.robot | 3 + .../FreonEnvironmentCredentialsProvider.java | 67 ++++++++++++ .../hadoop/ozone/freon/S3EntityGenerator.java | 3 +- .../hadoop/ozone/freon/S3KeyGenerator.java | 3 +- ...stFreonEnvironmentCredentialsProvider.java | 101 ++++++++++++++++++ .../s3/awssdk/v2/AbstractS3SDKV2Tests.java | 28 +++++ 6 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonEnvironmentCredentialsProvider.java create mode 100644 hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonEnvironmentCredentialsProvider.java diff --git a/hadoop-ozone/dist/src/main/smoketest/s3/freon.robot b/hadoop-ozone/dist/src/main/smoketest/s3/freon.robot index 24ba88bba62f..f722f396c9ce 100644 --- a/hadoop-ozone/dist/src/main/smoketest/s3/freon.robot +++ b/hadoop-ozone/dist/src/main/smoketest/s3/freon.robot @@ -52,6 +52,9 @@ Run Freon S3BG Run Freon S3KG [Setup] Setup aws credentials Freon S3KG s3kg-${BUCKET} + # the content type that freon set for its keys with AWS SDK v1 + ${result} = Execute AWSS3APICli head-object --bucket ${BUCKET} --key s3kg-${BUCKET}/0 + Should contain ${result} "ContentType": "text/plain" Run Freon S3KG MPU [Setup] Setup aws credentials diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonEnvironmentCredentialsProvider.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonEnvironmentCredentialsProvider.java new file mode 100644 index 000000000000..cc9ad72fd10b --- /dev/null +++ b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonEnvironmentCredentialsProvider.java @@ -0,0 +1,67 @@ +/* + * 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.hadoop.ozone.freon; + +import com.google.common.annotations.VisibleForTesting; +import java.util.function.UnaryOperator; +import org.apache.commons.lang3.StringUtils; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +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.SdkClientException; + +/** + * Credentials from environment variables, read the way AWS SDK for Java v1 read them, which freon used before: + * the access key from {@code AWS_ACCESS_KEY_ID}, or else {@code AWS_ACCESS_KEY}, the secret key from + * {@code AWS_SECRET_KEY}, or else {@code AWS_SECRET_ACCESS_KEY}, and the session token from {@code AWS_SESSION_TOKEN}, + * if set. The environment provider of SDK v2 only reads {@code AWS_ACCESS_KEY_ID} and {@code AWS_SECRET_ACCESS_KEY}. + */ +public final class FreonEnvironmentCredentialsProvider implements AwsCredentialsProvider { + + private final UnaryOperator environment; + + public FreonEnvironmentCredentialsProvider() { + this(System::getenv); + } + + @VisibleForTesting + FreonEnvironmentCredentialsProvider(UnaryOperator environment) { + this.environment = environment; + } + + @Override + public AwsCredentials resolveCredentials() { + final String accessKey = StringUtils.trim(get("AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY")); + final String secretKey = StringUtils.trim(get("AWS_SECRET_KEY", "AWS_SECRET_ACCESS_KEY")); + if (StringUtils.isEmpty(accessKey) || StringUtils.isEmpty(secretKey)) { + throw SdkClientException.create("Unable to load AWS credentials from environment variables " + + "(AWS_ACCESS_KEY_ID (or AWS_ACCESS_KEY) and AWS_SECRET_KEY (or AWS_SECRET_ACCESS_KEY))"); + } + final String sessionToken = StringUtils.trim(environment.apply("AWS_SESSION_TOKEN")); + return StringUtils.isEmpty(sessionToken) + ? AwsBasicCredentials.create(accessKey, secretKey) + : AwsSessionCredentials.create(accessKey, secretKey, sessionToken); + } + + /** @return the value of the variable, or of the alternate one if the variable is not set at all */ + private String get(String variable, String alternate) { + final String value = environment.apply(variable); + return value != null ? value : environment.apply(alternate); + } +} diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java index fab89c68fecc..fa988c30ea1c 100644 --- a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java +++ b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java @@ -19,7 +19,6 @@ import java.net.URI; import picocli.CommandLine; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.core.checksums.RequestChecksumCalculation; import software.amazon.awssdk.core.checksums.ResponseChecksumValidation; import software.amazon.awssdk.http.apache.ApacheHttpClient; @@ -43,7 +42,7 @@ protected void s3ClientInit() { S3ClientBuilder s3ClientBuilder = S3Client.builder() .httpClientBuilder(ApacheHttpClient.builder()) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + .credentialsProvider(new FreonEnvironmentCredentialsProvider()) // checksums only where S3 requires them: a load generator should not spend its time computing them .requestChecksumCalculation(RequestChecksumCalculation.WHEN_REQUIRED) .responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED) diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java index 6f04181f3399..835777ee6e31 100644 --- a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java +++ b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java @@ -122,7 +122,8 @@ private void createKey(long counter) throws Exception { .multipartUpload(m -> m.parts(parts))); } else { - getS3().putObject(r -> r.bucket(bucketName).key(generateObjectName(counter)), + // the content type that AWS SDK v1 set for the String content freon uploaded before + getS3().putObject(r -> r.bucket(bucketName).key(generateObjectName(counter)).contentType("text/plain"), RequestBody.fromBytes(content)); } diff --git a/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonEnvironmentCredentialsProvider.java b/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonEnvironmentCredentialsProvider.java new file mode 100644 index 000000000000..25c92a976e9a --- /dev/null +++ b/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonEnvironmentCredentialsProvider.java @@ -0,0 +1,101 @@ +/* + * 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.hadoop.ozone.freon; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.core.exception.SdkClientException; + +/** Tests for {@link FreonEnvironmentCredentialsProvider}: the variables and precedence of AWS SDK v1. */ +class TestFreonEnvironmentCredentialsProvider { + + private final Map environment = new HashMap<>(); + private final FreonEnvironmentCredentialsProvider provider = + new FreonEnvironmentCredentialsProvider(environment::get); + + @Test + void readsStandardVariables() { + environment.put("AWS_ACCESS_KEY_ID", "access"); + environment.put("AWS_SECRET_ACCESS_KEY", "secret"); + + final AwsCredentials credentials = provider.resolveCredentials(); + assertEquals("access", credentials.accessKeyId()); + assertEquals("secret", credentials.secretAccessKey()); + assertThat(credentials).isNotInstanceOf(AwsSessionCredentials.class); + } + + @Test + void readsAlternateVariables() { + environment.put("AWS_ACCESS_KEY", "access"); + environment.put("AWS_SECRET_KEY", "secret"); + + final AwsCredentials credentials = provider.resolveCredentials(); + assertEquals("access", credentials.accessKeyId()); + assertEquals("secret", credentials.secretAccessKey()); + } + + /** As in SDK v1: AWS_ACCESS_KEY_ID before AWS_ACCESS_KEY, but AWS_SECRET_KEY before AWS_SECRET_ACCESS_KEY. */ + @Test + void precedenceOfSdkV1() { + environment.put("AWS_ACCESS_KEY_ID", "access-id"); + environment.put("AWS_ACCESS_KEY", "access"); + environment.put("AWS_SECRET_ACCESS_KEY", "secret-access"); + environment.put("AWS_SECRET_KEY", "secret"); + + final AwsCredentials credentials = provider.resolveCredentials(); + assertEquals("access-id", credentials.accessKeyId()); + assertEquals("secret", credentials.secretAccessKey()); + } + + @Test + void trimsValuesAndReadsSessionToken() { + environment.put("AWS_ACCESS_KEY_ID", " access "); + environment.put("AWS_SECRET_ACCESS_KEY", " secret\n"); + environment.put("AWS_SESSION_TOKEN", " token "); + + final AwsCredentials credentials = provider.resolveCredentials(); + assertEquals("access", credentials.accessKeyId()); + assertEquals("secret", credentials.secretAccessKey()); + assertThat(credentials).isInstanceOf(AwsSessionCredentials.class); + assertEquals("token", ((AwsSessionCredentials) credentials).sessionToken()); + } + + @Test + void failsWithoutSecretKey() { + environment.put("AWS_ACCESS_KEY_ID", "access"); + + assertThrows(SdkClientException.class, provider::resolveCredentials); + } + + /** As in SDK v1, a variable that is set but empty does not fall back to the alternate variable. */ + @Test + void emptyVariableDoesNotFallBack() { + environment.put("AWS_ACCESS_KEY_ID", " "); + environment.put("AWS_ACCESS_KEY", "access"); + environment.put("AWS_SECRET_ACCESS_KEY", "secret"); + + assertThrows(SdkClientException.class, provider::resolveCredentials); + } +} diff --git a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java index a3938fe91c34..32296aa3955a 100644 --- a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java +++ b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java @@ -3159,6 +3159,31 @@ public void tearDown() { sdkHttpClient.close(); } + @ParameterizedTest + @ValueSource(strings = {"annotation", "torrent"}) + public void testPresignedUrlGetObjectSubresourceNotImplemented(String subresource) throws Exception { + final String keyName = getKeyName(); + s3Client.putObject(b -> b.bucket(BUCKET_NAME).key(keyName), RequestBody.fromString(CONTENT)); + + // a presigned GetObject with the subresource query parameter, as clients without the typed API send it + final GetObjectRequest objectRequest = GetObjectRequest.builder().bucket(BUCKET_NAME).key(keyName) + .overrideConfiguration(o -> o.putRawQueryParameter(subresource, "")) + .build(); + final URL presignedUrl = presigner.presignGetObject(b -> b.signatureDuration(duration) + .getObjectRequest(objectRequest)).url(); + assertThat(presignedUrl.getQuery()).contains(subresource); + + final HttpURLConnection connection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "GET", null, null); + try { + assertEquals(HttpURLConnection.HTTP_NOT_IMPLEMENTED, connection.getResponseCode()); + } finally { + connection.disconnect(); + } + + // the object must be untouched + assertEquals(CONTENT.length(), s3Client.headObject(b -> b.bucket(BUCKET_NAME).key(keyName)).contentLength()); + } + @Test public void testPresignedUrlGetWithCustomQueryParameter() throws Exception { final String keyName = getKeyName(); @@ -4424,6 +4449,9 @@ public void testGetParticularPart(@TempDir Path tempDir) throws Exception { final long allPartsLength = s3Client.getObjectAsBytes(b -> b.bucket(bucketName).key(keyName)) .response().contentLength(); + // part number 0 is not a part: the whole object is returned + assertEquals(allPartsLength, s3Client.getObjectAsBytes(b -> b.bucket(bucketName).key(keyName).partNumber(0)) + .response().contentLength()); long partsLength = 0; for (int part = 1; part <= 3; part++) { final int partNumber = part; From 4192052f37ce178faaebfe7d8798b2c42cd2804b Mon Sep 17 00:00:00 2001 From: Andrey Yarovoy Date: Thu, 5 Nov 2026 04:26:40 -0500 Subject: [PATCH 3/3] Removed aws sdk 2 usage from freon and made aws sdk 2 test only dependency Co-authored-by: Claude Opus 5.5 --- .../dist/src/main/license/bin/LICENSE.txt | 30 ---- .../dist/src/main/license/bin/NOTICE.txt | 30 ---- .../dist/src/main/license/jar-report.txt | 40 +----- .../src/main/smoketest/fcq/s3_om_fcq.robot | 27 ++-- .../dist/src/main/smoketest/s3/freon.robot | 61 -------- hadoop-ozone/freon/pom.xml | 43 ------ .../FreonEnvironmentCredentialsProvider.java | 67 --------- .../freon/FreonS3TraceContextInterceptor.java | 45 ------ .../hadoop/ozone/freon/S3BucketGenerator.java | 75 ---------- .../hadoop/ozone/freon/S3EntityGenerator.java | 70 --------- .../hadoop/ozone/freon/S3KeyGenerator.java | 133 ------------------ ...stFreonEnvironmentCredentialsProvider.java | 101 ------------- .../TestFreonS3TraceContextInterceptor.java | 73 ---------- 13 files changed, 14 insertions(+), 781 deletions(-) delete mode 100644 hadoop-ozone/dist/src/main/smoketest/s3/freon.robot delete mode 100644 hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonEnvironmentCredentialsProvider.java delete mode 100644 hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonS3TraceContextInterceptor.java delete mode 100644 hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3BucketGenerator.java delete mode 100644 hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java delete mode 100644 hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java delete mode 100644 hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonEnvironmentCredentialsProvider.java delete mode 100644 hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonS3TraceContextInterceptor.java diff --git a/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt b/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt index b8c36618f2a1..3f27f2610673 100644 --- a/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt +++ b/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt @@ -483,36 +483,6 @@ Apache License 2.0 org.xerial.snappy:snappy-java org.xerial:sqlite-jdbc org.yaml:snakeyaml - software.amazon.awssdk:annotations - software.amazon.awssdk:apache-client - software.amazon.awssdk:arns - software.amazon.awssdk:auth - software.amazon.awssdk:aws-core - software.amazon.awssdk:aws-query-protocol - software.amazon.awssdk:aws-xml-protocol - software.amazon.awssdk:checksums - software.amazon.awssdk:checksums-spi - software.amazon.awssdk:crt-core - software.amazon.awssdk:endpoints-spi - software.amazon.awssdk:http-auth - software.amazon.awssdk:http-auth-aws - software.amazon.awssdk:http-auth-aws-eventstream - software.amazon.awssdk:http-auth-spi - software.amazon.awssdk:http-client-spi - software.amazon.awssdk:identity-spi - software.amazon.awssdk:json-utils - software.amazon.awssdk:metrics-spi - software.amazon.awssdk:profiles - software.amazon.awssdk:protocol-core - software.amazon.awssdk:regions - software.amazon.awssdk:retries - software.amazon.awssdk:retries-spi - software.amazon.awssdk:s3 - software.amazon.awssdk:sdk-core - software.amazon.awssdk:third-party-jackson-core - software.amazon.awssdk:utils - software.amazon.awssdk:utils-lite - software.amazon.eventstream:eventstream MIT ===================== diff --git a/hadoop-ozone/dist/src/main/license/bin/NOTICE.txt b/hadoop-ozone/dist/src/main/license/bin/NOTICE.txt index c5c1fda48840..dd78c02888cf 100644 --- a/hadoop-ozone/dist/src/main/license/bin/NOTICE.txt +++ b/hadoop-ozone/dist/src/main/license/bin/NOTICE.txt @@ -448,36 +448,6 @@ This product includes software developed at The Apache Software Foundation (http://www.apache.org/). -software.amazon.awssdk -==================== - -AWS SDK for Java 2.0 -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - -This product includes software developed by -Amazon Technologies, Inc (http://www.amazon.com/). - -********************** -THIRD PARTY COMPONENTS -********************** -This software includes third party software subject to the following copyrights: -- XML parsing and utility functions from JetS3t - Copyright 2006-2009 James Murty. -- PKCS#1 PEM encoded private key parsing and utility functions from oauth.googlecode.com - Copyright 1998-2010 AOL Inc. -- Apache Commons Lang - https://github.com/apache/commons-lang -- Netty Reactive Streams - https://github.com/playframework/netty-reactive-streams -- Jackson-core - https://github.com/FasterXML/jackson-core -- Jackson-dataformat-cbor - https://github.com/FasterXML/jackson-dataformats-binary - -The licenses for these third party components are included in LICENSE.txt - -- For Apache Commons Lang see also this required NOTICE: - Apache Commons Lang - Copyright 2001-2020 The Apache Software Foundation - - This product includes software developed at - The Apache Software Foundation (https://www.apache.org/). - - ==================== ratis-thirdparty-misc is a shaded dependency which includes additional 3rd party dependencies in shaded form. For the detailed list of the dependencies and the associated NOTICE file see licenses/NOTICE-ratis-thirdparty-misc.txt. diff --git a/hadoop-ozone/dist/src/main/license/jar-report.txt b/hadoop-ozone/dist/src/main/license/jar-report.txt index 28874de0aa07..661b9c065cfc 100644 --- a/hadoop-ozone/dist/src/main/license/jar-report.txt +++ b/hadoop-ozone/dist/src/main/license/jar-report.txt @@ -1,34 +1,23 @@ share/ozone/lib/aircompressor.jar -share/ozone/lib/aircompressor.jar share/ozone/lib/animal-sniffer-annotations.jar share/ozone/lib/annotations.jar share/ozone/lib/annotations.jar -share/ozone/lib/annotations.jar -share/ozone/lib/annotations.jar share/ozone/lib/aopalliance-repackaged.jar share/ozone/lib/aopalliance.jar -share/ozone/lib/apache-client.jar share/ozone/lib/apache-log4j-extras.jar -share/ozone/lib/arns.jar share/ozone/lib/asm-analysis.jar share/ozone/lib/asm-commons.jar share/ozone/lib/asm-tree.jar share/ozone/lib/asm-util.jar share/ozone/lib/asm.jar share/ozone/lib/aspectjrt.jar -share/ozone/lib/auth.jar share/ozone/lib/avro.jar -share/ozone/lib/aws-core.jar -share/ozone/lib/aws-query-protocol.jar -share/ozone/lib/aws-xml-protocol.jar share/ozone/lib/bcpkix-jdk18on.jar share/ozone/lib/bcprov-jdk18on.jar share/ozone/lib/bcutil-jdk18on.jar share/ozone/lib/bonecp.RELEASE.jar share/ozone/lib/caffeine.jar share/ozone/lib/cdi-api.SP1.jar -share/ozone/lib/checksums-spi.jar -share/ozone/lib/checksums.jar share/ozone/lib/commons-beanutils.jar share/ozone/lib/commons-cli.jar share/ozone/lib/commons-codec.jar @@ -46,15 +35,12 @@ share/ozone/lib/commons-pool2.jar share/ozone/lib/commons-text.jar share/ozone/lib/commons-validator.jar share/ozone/lib/converter-jackson.jar -share/ozone/lib/crt-core.jar share/ozone/lib/curator-client.jar share/ozone/lib/curator-framework.jar share/ozone/lib/derby.jar share/ozone/lib/disruptor.jar share/ozone/lib/dnsjava.jar -share/ozone/lib/endpoints-spi.jar share/ozone/lib/error_prone_annotations.jar -share/ozone/lib/eventstream.jar share/ozone/lib/failureaccess.jar share/ozone/lib/grpc-api.jar share/ozone/lib/grpc-context.jar @@ -95,11 +81,6 @@ share/ozone/lib/hk2-api.jar share/ozone/lib/hk2-locator.jar share/ozone/lib/hk2-utils.jar share/ozone/lib/hppc.jar -share/ozone/lib/http-auth-aws-eventstream.jar -share/ozone/lib/http-auth-aws.jar -share/ozone/lib/http-auth-spi.jar -share/ozone/lib/http-auth.jar -share/ozone/lib/http-client-spi.jar share/ozone/lib/httpclient.jar share/ozone/lib/httpcore.jar share/ozone/lib/iceberg-api.jar @@ -108,7 +89,6 @@ share/ozone/lib/iceberg-common.jar share/ozone/lib/iceberg-core.jar share/ozone/lib/iceberg-orc.jar share/ozone/lib/iceberg-parquet.jar -share/ozone/lib/identity-spi.jar share/ozone/lib/istack-commons-runtime.jar share/ozone/lib/j2objc-annotations.jar share/ozone/lib/jackson-annotations.jar @@ -121,7 +101,6 @@ share/ozone/lib/jackson-jaxrs-json-provider.jar share/ozone/lib/jackson-module-jaxb-annotations.jar share/ozone/lib/jakarta.activation-api.jar share/ozone/lib/jakarta.activation.jar -share/ozone/lib/jakarta.activation.jar share/ozone/lib/jakarta.annotation-api.jar share/ozone/lib/jakarta.inject-api.jar share/ozone/lib/jakarta.inject.jar @@ -188,13 +167,12 @@ share/ozone/lib/jooq-codegen.jar share/ozone/lib/jooq-meta.jar share/ozone/lib/jooq.jar share/ozone/lib/jsch.jar -share/ozone/lib/json-utils.jar share/ozone/lib/jsp-api.jar share/ozone/lib/jspecify.jar share/ozone/lib/jsr311-api.jar -share/ozone/lib/jtokkit.jar share/ozone/lib/jts-core.jar share/ozone/lib/jul-to-slf4j.jar +share/ozone/lib/jtokkit.jar share/ozone/lib/kerb-core.jar share/ozone/lib/kerb-crypto.jar share/ozone/lib/kerb-util.jar @@ -213,7 +191,6 @@ share/ozone/lib/listenablefuture-empty-to-avoid-conflict-with-guava.jar share/ozone/lib/log4j-api.jar share/ozone/lib/log4j-core.jar share/ozone/lib/metrics-core.jar -share/ozone/lib/metrics-spi.jar share/ozone/lib/netty-buffer.Final.jar share/ozone/lib/netty-codec-http.Final.jar share/ozone/lib/netty-codec-http2.Final.jar @@ -239,7 +216,6 @@ share/ozone/lib/okhttp-jvm.jar share/ozone/lib/okhttp-sse.jar share/ozone/lib/okhttp.jar share/ozone/lib/okio-jvm.jar -share/ozone/lib/okio-jvm.jar share/ozone/lib/okio.jar share/ozone/lib/openai4j.jar share/ozone/lib/opentelemetry-api.jar @@ -295,14 +271,12 @@ share/ozone/lib/parquet-variant.jar share/ozone/lib/perfmark-api.jar share/ozone/lib/picocli-shell-jline3.jar share/ozone/lib/picocli.jar -share/ozone/lib/profiles.jar share/ozone/lib/proto-google-common-protos.jar share/ozone/lib/protobuf-java.jar -share/ozone/lib/protocol-core.jar -share/ozone/lib/r2dbc-spi.RELEASE.jar share/ozone/lib/ranger-audit-core.jar share/ozone/lib/ranger-authz-api.jar share/ozone/lib/ranger-common-utils.jar +share/ozone/lib/r2dbc-spi.RELEASE.jar share/ozone/lib/ranger-intg.jar share/ozone/lib/ranger-plugin-classloader.jar share/ozone/lib/ranger-plugins-common.jar @@ -321,17 +295,11 @@ share/ozone/lib/ratis-thirdparty-misc.jar share/ozone/lib/ratis-tools.jar share/ozone/lib/re2j.jar share/ozone/lib/reactive-streams.jar -share/ozone/lib/reactive-streams.jar share/ozone/lib/reflections.jar -share/ozone/lib/regions.jar share/ozone/lib/reload4j.jar -share/ozone/lib/retries-spi.jar -share/ozone/lib/retries.jar share/ozone/lib/retrofit.jar share/ozone/lib/rocksdb-checkpoint-differ.jar share/ozone/lib/rocksdbjni.jar -share/ozone/lib/s3.jar -share/ozone/lib/sdk-core.jar share/ozone/lib/simpleclient.jar share/ozone/lib/simpleclient_common.jar share/ozone/lib/simpleclient_dropwizard.jar @@ -345,12 +313,8 @@ share/ozone/lib/spring-jdbc.jar share/ozone/lib/spring-tx.jar share/ozone/lib/sqlite-jdbc.jar share/ozone/lib/stax2-api.jar -share/ozone/lib/stax2-api.jar -share/ozone/lib/third-party-jackson-core.jar share/ozone/lib/txw2.jar share/ozone/lib/ugsync-util.jar -share/ozone/lib/utils-lite.jar -share/ozone/lib/utils.jar share/ozone/lib/vault-java-driver.jar share/ozone/lib/weld-servlet-shaded.Final.jar share/ozone/lib/woodstox-core.jar diff --git a/hadoop-ozone/dist/src/main/smoketest/fcq/s3_om_fcq.robot b/hadoop-ozone/dist/src/main/smoketest/fcq/s3_om_fcq.robot index 256a9c3e8051..530aa2f55a4a 100644 --- a/hadoop-ozone/dist/src/main/smoketest/fcq/s3_om_fcq.robot +++ b/hadoop-ozone/dist/src/main/smoketest/fcq/s3_om_fcq.robot @@ -37,12 +37,6 @@ Setup headers Kinit test user testuser testuser.keytab Setup secure v4 headers -Setup aws credentials - ${accessKey} = Execute aws configure get aws_access_key_id - ${secret} = Execute aws configure get aws_secret_access_key - Set Environment Variable AWS_SECRET_ACCESS_KEY ${secret} - Set Environment Variable AWS_ACCESS_KEY_ID ${accessKey} - Verify endpoint is up [arguments] ${url} Run Keyword if '${SECURITY_ENABLED}' == 'true' Kinit HTTP user @@ -52,10 +46,16 @@ Verify endpoint is up Setup bucket1 Execute AWSS3APICli create-bucket --bucket bucket1 -Freon s3kg - [arguments] ${prefix}=s3bg ${n}=1000 ${threads}=10 ${args}=${EMPTY} - ${result} = Execute ozone freon s3kg -e ${S3_ENDPOINT_URL} -t ${threads} -n ${n} -p ${prefix} ${args} - Should contain ${result} Successful executions: ${n} +Generate S3 load + [arguments] ${n}=100 + # one aws process uploads all the files, several at a time + ${dir} = Execute mktemp -d + Execute for i in $(seq 1 ${n}); do echo "$i" > ${dir}/key-$i; done + ${result} = Execute AWSS3Cli cp --recursive ${dir} s3://bucket1/fcq/ + Should contain ${result} upload: + # count the objects: the CLI merges the pages of the listing, which drops KeyCount + ${count} = Execute AWSS3APICli list-objects-v2 --bucket bucket1 --prefix fcq/ --query 'length(Contents)' + Should Be Equal As Integers ${count} ${n} Check metrics caller ${result} = Execute curl --negotiate -u : -LSs ${OM_JMX_ENDPOINT} | sed -n '/${DECAY_RPC_SCHEDULER_METRICS_NAME}/,/}/p' | grep 'Caller(' @@ -65,9 +65,6 @@ Check metrics caller Test setup headers Setup headers -Test setup credentials - Setup aws credentials - Test create bucket 1 Setup bucket1 @@ -78,8 +75,8 @@ Test metrics are registered ${result} = Execute curl --negotiate -u : -LSs ${OM_JMX_ENDPOINT} | grep ${DECAY_RPC_SCHEDULER_METRICS_NAME} Should contain ${result} ${DECAY_RPC_SCHEDULER_METRICS_NAME} -Run freon s3kg - Freon s3kg +Run S3 load + Generate S3 load Test caller from metrics Check metrics caller diff --git a/hadoop-ozone/dist/src/main/smoketest/s3/freon.robot b/hadoop-ozone/dist/src/main/smoketest/s3/freon.robot deleted file mode 100644 index f722f396c9ce..000000000000 --- a/hadoop-ozone/dist/src/main/smoketest/s3/freon.robot +++ /dev/null @@ -1,61 +0,0 @@ -# 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. - -*** Settings *** -Documentation S3 gateway test with aws cli -Library OperatingSystem -Library String -Library BuiltIn -Resource ./commonawslib.robot -Suite Setup Setup s3 tests -Default Tags no-bucket-type - -*** Variables *** -${ENDPOINT_URL} http://s3g:9878 -${BUCKET} generated - -*** Keywords *** -# Export access key and secret to the environment -Setup aws credentials - ${accessKey} = Execute aws configure get aws_access_key_id - ${secret} = Execute aws configure get aws_secret_access_key - Set Environment Variable AWS_SECRET_ACCESS_KEY ${secret} - Set Environment Variable AWS_ACCESS_KEY_ID ${accessKey} - -Freon S3BG - [arguments] ${prefix}=s3bg ${n}=100 ${threads}=10 ${args}=${EMPTY} - ${result} = Execute ozone freon s3bg -e ${ENDPOINT_URL} -t ${threads} -n ${n} -p ${prefix} ${args} - Should contain ${result} Successful executions: ${n} - -Freon S3KG - [arguments] ${prefix}=s3kg ${n}=100 ${threads}=10 ${args}=${EMPTY} - ${result} = Execute ozone freon s3kg -e ${ENDPOINT_URL} -t ${threads} -n ${n} -p ${prefix} --bucket ${BUCKET} ${args} - Should contain ${result} Successful executions: ${n} - -*** Test Cases *** -Run Freon S3BG - [Setup] Setup aws credentials - Freon S3BG s3bg-${BUCKET} - -Run Freon S3KG - [Setup] Setup aws credentials - Freon S3KG s3kg-${BUCKET} - # the content type that freon set for its keys with AWS SDK v1 - ${result} = Execute AWSS3APICli head-object --bucket ${BUCKET} --key s3kg-${BUCKET}/0 - Should contain ${result} "ContentType": "text/plain" - -Run Freon S3KG MPU - [Setup] Setup aws credentials - Freon S3KG s3kg-mpu-${BUCKET} 10 1 --multi-part-upload --parts=2 --size=5242880 diff --git a/hadoop-ozone/freon/pom.xml b/hadoop-ozone/freon/pom.xml index 7b329a3bf827..7289cf2a202b 100644 --- a/hadoop-ozone/freon/pom.xml +++ b/hadoop-ozone/freon/pom.xml @@ -131,49 +131,6 @@ org.slf4j slf4j-api - - software.amazon.awssdk - apache-client - - - software.amazon.awssdk - auth - - - software.amazon.awssdk - aws-core - - - software.amazon.awssdk - http-client-spi - - - software.amazon.awssdk - regions - - - software.amazon.awssdk - s3 - - - - software.amazon.awssdk - apache5-client - - - software.amazon.awssdk - netty-nio-client - - - - - software.amazon.awssdk - sdk-core - - - software.amazon.awssdk - utils - org.apache.ozone hdds-annotation-processing diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonEnvironmentCredentialsProvider.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonEnvironmentCredentialsProvider.java deleted file mode 100644 index cc9ad72fd10b..000000000000 --- a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonEnvironmentCredentialsProvider.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.hadoop.ozone.freon; - -import com.google.common.annotations.VisibleForTesting; -import java.util.function.UnaryOperator; -import org.apache.commons.lang3.StringUtils; -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; -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.SdkClientException; - -/** - * Credentials from environment variables, read the way AWS SDK for Java v1 read them, which freon used before: - * the access key from {@code AWS_ACCESS_KEY_ID}, or else {@code AWS_ACCESS_KEY}, the secret key from - * {@code AWS_SECRET_KEY}, or else {@code AWS_SECRET_ACCESS_KEY}, and the session token from {@code AWS_SESSION_TOKEN}, - * if set. The environment provider of SDK v2 only reads {@code AWS_ACCESS_KEY_ID} and {@code AWS_SECRET_ACCESS_KEY}. - */ -public final class FreonEnvironmentCredentialsProvider implements AwsCredentialsProvider { - - private final UnaryOperator environment; - - public FreonEnvironmentCredentialsProvider() { - this(System::getenv); - } - - @VisibleForTesting - FreonEnvironmentCredentialsProvider(UnaryOperator environment) { - this.environment = environment; - } - - @Override - public AwsCredentials resolveCredentials() { - final String accessKey = StringUtils.trim(get("AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY")); - final String secretKey = StringUtils.trim(get("AWS_SECRET_KEY", "AWS_SECRET_ACCESS_KEY")); - if (StringUtils.isEmpty(accessKey) || StringUtils.isEmpty(secretKey)) { - throw SdkClientException.create("Unable to load AWS credentials from environment variables " - + "(AWS_ACCESS_KEY_ID (or AWS_ACCESS_KEY) and AWS_SECRET_KEY (or AWS_SECRET_ACCESS_KEY))"); - } - final String sessionToken = StringUtils.trim(environment.apply("AWS_SESSION_TOKEN")); - return StringUtils.isEmpty(sessionToken) - ? AwsBasicCredentials.create(accessKey, secretKey) - : AwsSessionCredentials.create(accessKey, secretKey, sessionToken); - } - - /** @return the value of the variable, or of the alternate one if the variable is not set at all */ - private String get(String variable, String alternate) { - final String value = environment.apply(variable); - return value != null ? value : environment.apply(alternate); - } -} diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonS3TraceContextInterceptor.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonS3TraceContextInterceptor.java deleted file mode 100644 index 58da089cb0f1..000000000000 --- a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/FreonS3TraceContextInterceptor.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.hadoop.ozone.freon; - -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import software.amazon.awssdk.core.interceptor.Context; -import software.amazon.awssdk.core.interceptor.ExecutionAttributes; -import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; -import software.amazon.awssdk.http.SdkHttpRequest; - -/** - * Adds W3C trace context headers to each outgoing S3 request so the S3 Gateway - * can attach its spans to the Freon task span, using {@link W3CTraceContextPropagator}. - */ -public final class FreonS3TraceContextInterceptor implements ExecutionInterceptor { - - @Override - public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { - if (!Span.current().getSpanContext().isValid()) { - return context.httpRequest(); - } - final SdkHttpRequest.Builder request = context.httpRequest().toBuilder(); - W3CTraceContextPropagator.getInstance().inject( - io.opentelemetry.context.Context.current(), - request, - (carrier, key, value) -> carrier.putHeader(key, value)); - return request.build(); - } -} diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3BucketGenerator.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3BucketGenerator.java deleted file mode 100644 index e6ba1c5ad3d9..000000000000 --- a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3BucketGenerator.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.hadoop.ozone.freon; - -import com.codahale.metrics.Timer; -import java.util.concurrent.Callable; -import org.apache.hadoop.hdds.cli.HddsVersionProvider; -import org.kohsuke.MetaInfServices; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; - -/** - * Generate buckets via the s3 interface. - * - * For a secure cluster, - * $> init user keytab - * $> kinit -kt /etc/security/keytabs/testuser.keytab testuser/scm@EXAMPLE.COM - * $> eval $(ozone s3 getsecret -e) - * for getting and exporting access_key_id and secret_access_key - * to freon shell test environment - * secret access key. - */ -@Command(name = "s3bg", - aliases = "s3-bucket-generator", - description = "Create buckets via the s3 interface.%n" + - "Command requires AWS authentication environment " + - "variables set, AWS_ACCESS_KEY_ID and " + - "AWS_SECRET_ACCESS_KEY", - versionProvider = HddsVersionProvider.class, - mixinStandardHelpOptions = true, - showDefaultValues = true) -@MetaInfServices(FreonSubcommand.class) -public class S3BucketGenerator extends S3EntityGenerator - implements Callable { - - private static final Logger LOG = - LoggerFactory.getLogger(S3BucketGenerator.class); - - private Timer timer; - - @Override - public Void call() throws Exception { - s3ClientInit(); - - timer = getMetrics().timer("bucket-create"); - - runTests(this::createBucket); - - return null; - } - - private void createBucket(long bucketNum) throws Exception { - String bName = getPrefix() + bucketNum; - timer.time(() -> { - getS3().createBucket(r -> r.bucket(bName)); - return null; - }); - } -} diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java deleted file mode 100644 index fa988c30ea1c..000000000000 --- a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3EntityGenerator.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.hadoop.ozone.freon; - -import java.net.URI; -import picocli.CommandLine; -import software.amazon.awssdk.core.checksums.RequestChecksumCalculation; -import software.amazon.awssdk.core.checksums.ResponseChecksumValidation; -import software.amazon.awssdk.http.apache.ApacheHttpClient; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.S3ClientBuilder; - -/** - * Initiliazing common aspects of S3BucketGenerator and S3KeyGenerator. - */ -public class S3EntityGenerator extends BaseFreonGenerator { - @CommandLine.Option(names = {"-e", "--endpoint"}, - description = "S3 HTTP endpoint", - defaultValue = "http://localhost:9878") - private String endpoint; - - private S3Client s3; - - protected void s3ClientInit() { - init(); - - S3ClientBuilder s3ClientBuilder = S3Client.builder() - .httpClientBuilder(ApacheHttpClient.builder()) - .credentialsProvider(new FreonEnvironmentCredentialsProvider()) - // checksums only where S3 requires them: a load generator should not spend its time computing them - .requestChecksumCalculation(RequestChecksumCalculation.WHEN_REQUIRED) - .responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED) - .overrideConfiguration(c -> c.addExecutionInterceptor(new FreonS3TraceContextInterceptor())); - - if (!endpoint.isEmpty()) { - s3ClientBuilder - .forcePathStyle(true) - .endpointOverride(URI.create(endpoint)) - .region(Region.US_EAST_1); - } else { - s3ClientBuilder.region(Region.US_WEST_2); - } - - s3 = s3ClientBuilder.build(); - } - - public String getEndpoint() { - return endpoint; - } - - public S3Client getS3() { - return s3; - } -} diff --git a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java b/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java deleted file mode 100644 index 835777ee6e31..000000000000 --- a/hadoop-ozone/freon/src/main/java/org/apache/hadoop/ozone/freon/S3KeyGenerator.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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.hadoop.ozone.freon; - -import static org.apache.hadoop.ozone.OzoneConsts.OM_MULTIPART_MIN_SIZE; - -import com.codahale.metrics.Timer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import org.apache.commons.lang3.RandomStringUtils; -import org.apache.hadoop.hdds.cli.HddsVersionProvider; -import org.kohsuke.MetaInfServices; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.services.s3.model.CompletedPart; -import software.amazon.awssdk.services.s3.model.UploadPartResponse; - -/** - * Generate random keys via the s3 interface. - */ -@Command(name = "s3kg", - aliases = "s3-key-generator", - description = "Create random keys via the s3 interface." + - "Command requires AWS authentication environment " + - "variables set, AWS_ACCESS_KEY_ID and " + - "AWS_SECRET_ACCESS_KEY", - versionProvider = HddsVersionProvider.class, - mixinStandardHelpOptions = true, - showDefaultValues = true) -@MetaInfServices(FreonSubcommand.class) -@SuppressWarnings("java:S2245") // no need for secure random -public class S3KeyGenerator extends S3EntityGenerator - implements Callable { - - private static final Logger LOG = - LoggerFactory.getLogger(S3KeyGenerator.class); - - @Option(names = {"-b", "--bucket"}, - description = - "Name of the (S3!) bucket which contains the test data.", - defaultValue = "bucket1") - private String bucketName; - - @Option(names = {"-s", "--size"}, - description = "Size of the generated key (in bytes) or size of one " - + "multipart upload part (in case of multipart upload)", - defaultValue = "10240") - private int fileSize; - - @Option(names = {"--multi-part-upload"}, - description = "Use multi-part upload", - defaultValue = "false") - private boolean multiPart; - - @Option(names = {"--parts"}, - description = "Number of parts for multipart upload (final size = " - + "--size * --parts)", - defaultValue = "10") - private int numberOfParts; - - private Timer timer; - - private byte[] content; - - @Override - public Void call() throws Exception { - - if (multiPart && fileSize < OM_MULTIPART_MIN_SIZE) { - throw new IllegalArgumentException( - "Size of multipart upload parts should be at least 5MB (5242880)"); - } - - s3ClientInit(); - - content = RandomStringUtils.secure().nextAscii(fileSize).getBytes(StandardCharsets.UTF_8); - - timer = getMetrics().timer("key-create"); - - runTests(this::createKey); - - return null; - } - - private void createKey(long counter) throws Exception { - timer.time(() -> { - if (multiPart) { - - final String keyName = generateObjectName(counter); - final String uploadId = getS3().createMultipartUpload(r -> r.bucket(bucketName).key(keyName)).uploadId(); - - List parts = new ArrayList<>(); - for (int i = 1; i <= numberOfParts; i++) { - final int partNumber = i; - final UploadPartResponse uploadPartResponse = getS3().uploadPart( - r -> r.bucket(bucketName).key(keyName).uploadId(uploadId).partNumber(partNumber) - .contentLength((long) content.length), - RequestBody.fromBytes(content)); - parts.add(CompletedPart.builder().partNumber(partNumber).eTag(uploadPartResponse.eTag()).build()); - } - - getS3().completeMultipartUpload(r -> r.bucket(bucketName).key(keyName).uploadId(uploadId) - .multipartUpload(m -> m.parts(parts))); - - } else { - // the content type that AWS SDK v1 set for the String content freon uploaded before - getS3().putObject(r -> r.bucket(bucketName).key(generateObjectName(counter)).contentType("text/plain"), - RequestBody.fromBytes(content)); - } - - return null; - }); - } -} diff --git a/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonEnvironmentCredentialsProvider.java b/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonEnvironmentCredentialsProvider.java deleted file mode 100644 index 25c92a976e9a..000000000000 --- a/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonEnvironmentCredentialsProvider.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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.hadoop.ozone.freon; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.HashMap; -import java.util.Map; -import org.junit.jupiter.api.Test; -import software.amazon.awssdk.auth.credentials.AwsCredentials; -import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; -import software.amazon.awssdk.core.exception.SdkClientException; - -/** Tests for {@link FreonEnvironmentCredentialsProvider}: the variables and precedence of AWS SDK v1. */ -class TestFreonEnvironmentCredentialsProvider { - - private final Map environment = new HashMap<>(); - private final FreonEnvironmentCredentialsProvider provider = - new FreonEnvironmentCredentialsProvider(environment::get); - - @Test - void readsStandardVariables() { - environment.put("AWS_ACCESS_KEY_ID", "access"); - environment.put("AWS_SECRET_ACCESS_KEY", "secret"); - - final AwsCredentials credentials = provider.resolveCredentials(); - assertEquals("access", credentials.accessKeyId()); - assertEquals("secret", credentials.secretAccessKey()); - assertThat(credentials).isNotInstanceOf(AwsSessionCredentials.class); - } - - @Test - void readsAlternateVariables() { - environment.put("AWS_ACCESS_KEY", "access"); - environment.put("AWS_SECRET_KEY", "secret"); - - final AwsCredentials credentials = provider.resolveCredentials(); - assertEquals("access", credentials.accessKeyId()); - assertEquals("secret", credentials.secretAccessKey()); - } - - /** As in SDK v1: AWS_ACCESS_KEY_ID before AWS_ACCESS_KEY, but AWS_SECRET_KEY before AWS_SECRET_ACCESS_KEY. */ - @Test - void precedenceOfSdkV1() { - environment.put("AWS_ACCESS_KEY_ID", "access-id"); - environment.put("AWS_ACCESS_KEY", "access"); - environment.put("AWS_SECRET_ACCESS_KEY", "secret-access"); - environment.put("AWS_SECRET_KEY", "secret"); - - final AwsCredentials credentials = provider.resolveCredentials(); - assertEquals("access-id", credentials.accessKeyId()); - assertEquals("secret", credentials.secretAccessKey()); - } - - @Test - void trimsValuesAndReadsSessionToken() { - environment.put("AWS_ACCESS_KEY_ID", " access "); - environment.put("AWS_SECRET_ACCESS_KEY", " secret\n"); - environment.put("AWS_SESSION_TOKEN", " token "); - - final AwsCredentials credentials = provider.resolveCredentials(); - assertEquals("access", credentials.accessKeyId()); - assertEquals("secret", credentials.secretAccessKey()); - assertThat(credentials).isInstanceOf(AwsSessionCredentials.class); - assertEquals("token", ((AwsSessionCredentials) credentials).sessionToken()); - } - - @Test - void failsWithoutSecretKey() { - environment.put("AWS_ACCESS_KEY_ID", "access"); - - assertThrows(SdkClientException.class, provider::resolveCredentials); - } - - /** As in SDK v1, a variable that is set but empty does not fall back to the alternate variable. */ - @Test - void emptyVariableDoesNotFallBack() { - environment.put("AWS_ACCESS_KEY_ID", " "); - environment.put("AWS_ACCESS_KEY", "access"); - environment.put("AWS_SECRET_ACCESS_KEY", "secret"); - - assertThrows(SdkClientException.class, provider::resolveCredentials); - } -} diff --git a/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonS3TraceContextInterceptor.java b/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonS3TraceContextInterceptor.java deleted file mode 100644 index 8cb6b8199d1f..000000000000 --- a/hadoop-ozone/freon/src/test/java/org/apache/hadoop/ozone/freon/TestFreonS3TraceContextInterceptor.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.hadoop.ozone.freon; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.api.trace.TraceFlags; -import io.opentelemetry.api.trace.TraceState; -import io.opentelemetry.context.Scope; -import java.net.URI; -import org.junit.jupiter.api.Test; -import software.amazon.awssdk.core.interceptor.Context; -import software.amazon.awssdk.core.interceptor.ExecutionAttributes; -import software.amazon.awssdk.http.SdkHttpMethod; -import software.amazon.awssdk.http.SdkHttpRequest; - -/** Tests for {@link FreonS3TraceContextInterceptor}. */ -class TestFreonS3TraceContextInterceptor { - private static final String TRACE_ID = "0af7651916cd43dd8448eb211c80319c"; - private static final String SPAN_ID = "b7ad6b7169203331"; - - private final FreonS3TraceContextInterceptor interceptor = new FreonS3TraceContextInterceptor(); - - private static Context.ModifyHttpRequest context(SdkHttpRequest request) { - final Context.ModifyHttpRequest context = mock(Context.ModifyHttpRequest.class); - when(context.httpRequest()).thenReturn(request); - return context; - } - - private static SdkHttpRequest request() { - return SdkHttpRequest.builder().method(SdkHttpMethod.PUT).uri(URI.create("http://localhost:9878/bucket/key")) - .build(); - } - - @Test - void addsTraceContextOfCurrentSpan() { - final Span span = Span.wrap( - SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); - final SdkHttpRequest modified; - try (Scope ignored = span.makeCurrent()) { - modified = interceptor.modifyHttpRequest(context(request()), new ExecutionAttributes()); - } - - assertThat(modified.firstMatchingHeader("traceparent")).hasValue("00-" + TRACE_ID + "-" + SPAN_ID + "-01"); - } - - @Test - void leavesRequestUnchangedWithoutSpan() { - final SdkHttpRequest request = request(); - - assertSame(request, interceptor.modifyHttpRequest(context(request), new ExecutionAttributes())); - } -}