This is an automated email from the ASF dual-hosted git repository.
Gargi-jais11 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new e09eeaa7285 HDDS-15616. ListBuckets API ignores MaxBuckets parameter
and returns all buckets (#10639)
e09eeaa7285 is described below
commit e09eeaa728577552e08c4e4fdd8be5ae53b47dec
Author: Gargi Jaiswal <[email protected]>
AuthorDate: Tue Jul 14 14:32:34 2026 +0530
HDDS-15616. ListBuckets API ignores MaxBuckets parameter and returns all
buckets (#10639)
---
.../hadoop/ozone/s3/awssdk/S3SDKTestUtils.java | 59 +++++++
.../ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java | 169 ++++++++++++++++++--
.../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java | 170 +++++++++++++++++++--
.../ozone/s3/endpoint/ListBucketResponse.java | 13 +-
.../hadoop/ozone/s3/endpoint/RootEndpoint.java | 41 ++++-
.../org/apache/hadoop/ozone/s3/util/S3Consts.java | 3 +
.../hadoop/ozone/s3/endpoint/TestRootList.java | 94 ++++++++++++
7 files changed, 515 insertions(+), 34 deletions(-)
diff --git
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/S3SDKTestUtils.java
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/S3SDKTestUtils.java
index ec3793c68d1..2b3375052ce 100644
---
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/S3SDKTestUtils.java
+++
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/S3SDKTestUtils.java
@@ -25,12 +25,17 @@
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.ozone.test.InputSubstream;
@@ -49,6 +54,60 @@ public final class S3SDKTestUtils {
public static final Pattern UPLOAD_ID_PATTERN =
Pattern.compile("<UploadId>(.+?)</UploadId>");
+ /**
+ * One page of a paginated ListBuckets response.
+ */
+ public static final class BucketListPage {
+ private final List<String> bucketNames;
+ private final String continuationToken;
+
+ public BucketListPage(List<String> bucketNames, String continuationToken) {
+ this.bucketNames = bucketNames;
+ this.continuationToken = continuationToken;
+ }
+
+ public List<String> getBucketNames() {
+ return bucketNames;
+ }
+
+ public String getContinuationToken() {
+ return continuationToken;
+ }
+ }
+
+ /**
+ * Lists buckets one per page and returns all bucket names from the pages.
+ *
+ * @param listPage fetches one page; first arg is continuation token
(nullable),
+ * second arg is max buckets per page
+ */
+ public static List<String> collectBucketsOnePerPage(
+ BiFunction<String, Integer, BucketListPage> listPage) {
+ List<String> found = new ArrayList<>();
+ String continuationToken = null;
+ do {
+ BucketListPage page = listPage.apply(continuationToken, 1);
+ if (page.getBucketNames().size() != 1) {
+ throw new AssertionError(
+ "Expected 1 bucket per page, got " + page.getBucketNames().size());
+ }
+ found.add(page.getBucketNames().get(0));
+ continuationToken = page.getContinuationToken();
+ } while (continuationToken != null);
+ return found;
+ }
+
+ /**
+ * Filters a paginated bucket list down to the buckets created by the test.
+ */
+ public static List<String> filterToExpectedBuckets(
+ List<String> paginatedBuckets, String... expectedBuckets) {
+ Set<String> expected = new HashSet<>(Arrays.asList(expectedBuckets));
+ return paginatedBuckets.stream()
+ .filter(expected::contains)
+ .collect(Collectors.toList());
+ }
+
private S3SDKTestUtils() {
}
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
index c31e3154c32..618dc73212e 100644
---
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
@@ -55,6 +55,8 @@
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;
@@ -267,28 +269,163 @@ public void testBucketACLOperations() {
//assertEquals(aclList, s3Client.getBucketAcl(bucketName));
}
- @Test
- public void testListBuckets() throws IOException {
- List<String> bucketNames = new ArrayList<>();
- for (int i = 0; i <= 5; i++) {
- String bucketName = getBucketName(String.valueOf(i));
- s3Client.createBucket(bucketName);
- bucketNames.add(bucketName);
+ /**
+ * Integration tests for ListBuckets (GET / ListAllMyBuckets).
+ */
+ @Nested
+ class ListBucketsTests {
+
+ @Test
+ public void testListBuckets() throws IOException {
+ List<String> bucketNames = new ArrayList<>();
+ for (int i = 0; i <= 5; i++) {
+ String bucketName = getBucketName(String.valueOf(i));
+ s3Client.createBucket(bucketName);
+ bucketNames.add(bucketName);
+ }
+
+ List<Bucket> bucketList = s3Client.listBuckets();
+ List<String> 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<String> 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<String> 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<String> created = new ArrayList<>();
+
+ for (int i = 0; i < totalBuckets; i++) {
+ String name = uniqueObjectName("paginated-" + i);
+ s3Client.createBucket(name);
+ created.add(name);
+ }
+
+ try {
+ List<String> 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);
+ }
+ }
}
- List<Bucket> bucketList = s3Client.listBuckets();
- List<String> listBucketNames = bucketList.stream()
- .map(Bucket::getName).collect(Collectors.toList());
+ /**
+ * verifies that Page 1 uses maxBuckets=1;
+ * later pages send only continuationToken (no maxBuckets).
+ */
+ @Test
+ public void testListBucketsContinuationTokenWithoutMaxBuckets() {
+ List<String> created = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ String name = uniqueObjectName("token-only-" + i);
+ s3Client.createBucket(name);
+ created.add(name);
+ }
- assertThat(listBucketNames).containsAll(bucketNames);
+ try {
+ List<String> 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);
+ }
- UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
- String expectOwner = ugi.getShortUserName();
+ ListBucketsPaginatedResult response = s3Client.listBuckets(request);
+ if (continuationToken == null) {
+ assertEquals(1, response.getBuckets().size());
+ assertNotNull(response.getContinuationToken());
+ } else {
+ assertFalse(response.getBuckets().isEmpty());
+ }
- Owner s3AccountOwner = s3Client.getS3AccountOwner();
+ response.getBuckets().stream()
+ .map(Bucket::getName)
+ .filter(created::contains)
+ .forEach(retrieved::add);
- assertThat(s3AccountOwner.getDisplayName()).isEqualTo(expectOwner);
- assertThat(s3AccountOwner.getId()).isEqualTo(S3Owner.DEFAULT_S3OWNER_ID);
+ continuationToken = response.getContinuationToken();
+ } while (continuationToken != null);
+
+ assertThat(retrieved).containsExactlyInAnyOrderElementsOf(created);
+ } finally {
+ for (String name : created) {
+ s3Client.deleteBucket(name);
+ }
+ }
+ }
}
@Test
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 53cb48e99e9..ecab86ed323 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
@@ -131,6 +131,7 @@
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
+import software.amazon.awssdk.services.s3.model.ListBucketsRequest;
import software.amazon.awssdk.services.s3.model.ListBucketsResponse;
import software.amazon.awssdk.services.s3.model.ListDirectoryBucketsRequest;
import software.amazon.awssdk.services.s3.model.ListDirectoryBucketsResponse;
@@ -217,20 +218,6 @@ void closeClient() {
}
}
- @Test
- public void listBuckets() throws Exception {
- final String bucketName = getBucketName();
- final String expectedOwner =
UserGroupInformation.getCurrentUser().getUserName();
-
- s3Client.createBucket(b -> b.bucket(bucketName));
-
- ListBucketsResponse syncResponse = s3Client.listBuckets();
- assertEquals(1, syncResponse.buckets().size());
- assertEquals(bucketName, syncResponse.buckets().get(0).name());
- assertEquals(expectedOwner, syncResponse.owner().displayName());
- assertEquals(S3Owner.DEFAULT_S3OWNER_ID, syncResponse.owner().id());
- }
-
@Test
public void testPutObject() {
final String bucketName = getBucketName();
@@ -2857,6 +2844,161 @@ private void
verifyBucketOwnershipVerificationAccessDenied(Executable function)
}
}
+ /**
+ * Integration tests for ListBuckets (GET / ListAllMyBuckets).
+ */
+ @Nested
+ class ListBucketsTests {
+
+ @Test
+ public void testListBuckets() throws Exception {
+ List<String> bucketNames = new ArrayList<>();
+ for (int i = 0; i <= 5; i++) {
+ String bucketName = getBucketName(String.valueOf(i));
+ s3Client.createBucket(b -> b.bucket(bucketName));
+ bucketNames.add(bucketName);
+ }
+
+ ListBucketsResponse syncResponse = s3Client.listBuckets();
+ List<String> listBucketNames = syncResponse.buckets().stream()
+ .map(Bucket::name)
+ .collect(Collectors.toList());
+
+ assertThat(listBucketNames).containsAll(bucketNames);
+
+ String expectedOwner =
UserGroupInformation.getCurrentUser().getShortUserName();
+ assertEquals(expectedOwner, syncResponse.owner().displayName());
+ assertEquals(S3Owner.DEFAULT_S3OWNER_ID, syncResponse.owner().id());
+ }
+
+ /**
+ * Verifies {@code maxBuckets=1} returns one bucket per page and a
continuation token
+ * when more buckets exist.
+ */
+ @Test
+ public void testListBucketsPaginatedMaxBucketsOne() throws Exception {
+ final String bucketA = uniqueObjectName("bucket-a");
+ final String bucketB = uniqueObjectName("bucket-b");
+ s3Client.createBucket(b -> b.bucket(bucketA));
+ s3Client.createBucket(b -> b.bucket(bucketB));
+ try {
+ List<String> found = S3SDKTestUtils.collectBucketsOnePerPage((token,
max) -> {
+ ListBucketsRequest.Builder reqBuilder = ListBucketsRequest.builder()
+ .maxBuckets(max);
+ if (token != null) {
+ reqBuilder.continuationToken(token);
+ }
+ ListBucketsResponse page = s3Client.listBuckets(reqBuilder.build());
+ return new S3SDKTestUtils.BucketListPage(
+
page.buckets().stream().map(Bucket::name).collect(Collectors.toList()),
+ page.continuationToken());
+ });
+ List<String> foundTestBuckets = S3SDKTestUtils.filterToExpectedBuckets(
+ found, bucketA, bucketB);
+ assertThat(foundTestBuckets).containsExactlyInAnyOrder(bucketA,
bucketB);
+ } finally {
+ s3Client.deleteBucket(b -> b.bucket(bucketA));
+ s3Client.deleteBucket(b -> b.bucket(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() throws Exception {
+ final int totalBuckets = 5;
+ final int pageSize = 2;
+ List<String> created = new ArrayList<>();
+
+ for (int i = 0; i < totalBuckets; i++) {
+ String name = uniqueObjectName("paginated-" + i);
+ s3Client.createBucket(b -> b.bucket(name));
+ created.add(name);
+ }
+
+ try {
+ List<String> retrieved = new ArrayList<>();
+ String continuationToken = null;
+
+ do {
+ ListBucketsRequest.Builder reqBuilder = ListBucketsRequest.builder()
+ .maxBuckets(pageSize);
+ if (continuationToken != null) {
+ reqBuilder.continuationToken(continuationToken);
+ }
+
+ ListBucketsResponse response =
s3Client.listBuckets(reqBuilder.build());
+
+ response.buckets().stream()
+ .map(Bucket::name)
+ .filter(created::contains)
+ .forEach(retrieved::add);
+
+ continuationToken = response.continuationToken();
+ } while (continuationToken != null);
+
+ assertThat(retrieved).containsExactlyInAnyOrderElementsOf(created);
+ } finally {
+ for (String name : created) {
+ s3Client.deleteBucket(b -> b.bucket(name));
+ }
+ }
+ }
+
+ /**
+ * Verifies that page 2 can use only {@code continuationToken} without
{@code maxBuckets}.
+ * The first page uses {@code maxBuckets=1}; subsequent pages send only
the token.
+ */
+ @Test
+ public void testListBucketsContinuationTokenWithoutMaxBuckets() throws
Exception {
+ List<String> created = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ String name = uniqueObjectName("token-only-" + i);
+ s3Client.createBucket(b -> b.bucket(name));
+ created.add(name);
+ }
+
+ try {
+ List<String> retrieved = new ArrayList<>();
+ String continuationToken = null;
+ boolean firstPage = true;
+
+ do {
+ ListBucketsRequest.Builder reqBuilder = ListBucketsRequest.builder();
+ if (firstPage) {
+ reqBuilder.maxBuckets(1);
+ firstPage = false;
+ } else {
+ reqBuilder.continuationToken(continuationToken);
+ }
+
+ ListBucketsResponse response =
s3Client.listBuckets(reqBuilder.build());
+ if (continuationToken == null) {
+ assertEquals(1, response.buckets().size());
+ assertNotNull(response.continuationToken());
+ } else {
+ assertFalse(response.buckets().isEmpty());
+ }
+
+ response.buckets().stream()
+ .map(Bucket::name)
+ .filter(created::contains)
+ .forEach(retrieved::add);
+
+ continuationToken = response.continuationToken();
+ } while (continuationToken != null);
+
+ assertThat(retrieved).containsExactlyInAnyOrderElementsOf(created);
+ } finally {
+ for (String name : created) {
+ s3Client.deleteBucket(b -> b.bucket(name));
+ }
+ }
+ }
+ }
+
/**
* Integration tests for the ListDirectoryBuckets S3 API (HDDS-15450).
*
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListBucketResponse.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListBucketResponse.java
index 6515f1f9dda..30db1de61b4 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListBucketResponse.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListBucketResponse.java
@@ -41,7 +41,10 @@ public class ListBucketResponse {
@XmlElement(name = "Owner")
private S3Owner owner;
-
+
+ @XmlElement(name = "ContinuationToken")
+ private String continuationToken;
+
public List<BucketMetadata> getBuckets() {
return buckets;
}
@@ -66,4 +69,12 @@ public S3Owner getOwner() {
public void setOwner(S3Owner owner) {
this.owner = owner;
}
+
+ public String getContinuationToken() {
+ return continuationToken;
+ }
+
+ public void setContinuationToken(String continuationToken) {
+ this.continuationToken = continuationToken;
+ }
}
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
index 4cb52eaf3d5..e89d09a5432 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
@@ -153,22 +153,47 @@ private Response listAllBuckets()
long startNanos = Time.monotonicNowNanos();
boolean auditSuccess = true;
try {
+ final String continueToken =
queryParams().get(QueryParams.CONTINUATION_TOKEN);
+ final String maxBucketsParam =
queryParams().get(QueryParams.MAX_BUCKETS);
+ final boolean paginated = maxBucketsParam != null || continueToken !=
null;
+ final int maxBuckets = getMaxBuckets(paginated,
+ queryParams().getInt(QueryParams.MAX_BUCKETS,
S3Consts.MAX_BUCKETS_LIMIT));
+
ListBucketResponse response = new ListBucketResponse();
+ String previousBucket = null;
+ if (continueToken != null) {
+ previousBucket =
ContinueToken.decodeFromString(continueToken).getLastKey();
+ }
+
Iterator<? extends OzoneBucket> bucketIterator;
try {
- bucketIterator =
- listS3Buckets(null, volume ->
response.setOwner(S3Owner.of(volume.getOwner())));
+ if (previousBucket == null) {
+ bucketIterator = listS3Buckets(null,
+ volume -> response.setOwner(S3Owner.of(volume.getOwner())));
+ } else {
+ bucketIterator = listS3Buckets(null, previousBucket,
+ volume -> response.setOwner(S3Owner.of(volume.getOwner())));
+ }
} catch (Exception e) {
getMetrics().updateListS3BucketsFailureStats(startNanos);
throw e;
}
- while (bucketIterator.hasNext()) {
+ int count = 0;
+ String lastBucketName = null;
+ while (bucketIterator.hasNext() && count < maxBuckets) {
OzoneBucket next = bucketIterator.next();
BucketMetadata bucketMetadata = new BucketMetadata();
bucketMetadata.setName(next.getName());
bucketMetadata.setCreationDate(next.getCreationTime());
response.addBucket(bucketMetadata);
+ lastBucketName = next.getName();
+ count++;
+ }
+
+ if (paginated && lastBucketName != null && bucketIterator.hasNext()) {
+ response.setContinuationToken(
+ new ContinueToken(lastBucketName, null).encodeToString());
}
getMetrics().updateListS3BucketsSuccessStats(startNanos);
@@ -184,6 +209,16 @@ private Response listAllBuckets()
}
}
+ private int getMaxBuckets(boolean paginated, int requestedMaxBuckets) throws
OS3Exception {
+ if (!paginated) {
+ return Integer.MAX_VALUE;
+ }
+ if (requestedMaxBuckets < 1) {
+ throw newError(INVALID_ARGUMENT, "max-buckets must be >= 1");
+ }
+ return Math.min(requestedMaxBuckets, S3Consts.MAX_BUCKETS_LIMIT);
+ }
+
private int validateMaxDirectoryBuckets(int maxDirectoryBuckets) throws
OS3Exception {
if (maxDirectoryBuckets < 0) {
throw newError(INVALID_ARGUMENT, "max-directory-buckets must be >= 0");
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java
index 92266adbcab..df1a5e279d2 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java
@@ -115,6 +115,8 @@ public final class S3Consts {
// Constants related to S3 Express / ListDirectoryBuckets
public static final int MAX_DIRECTORY_BUCKETS_LIMIT = 1000;
+ // Maximum number of buckets per ListBuckets response page.
+ public static final int MAX_BUCKETS_LIMIT = 10000;
public static final String DEFAULT_S3_REGION = "us-east-1";
public static final String S3_EXPRESS_SERVICE = "s3express";
@@ -146,6 +148,7 @@ public static final class QueryParams {
public static final String LOCATION = "location";
public static final String MARKER = "marker";
public static final String MAX_DIRECTORY_BUCKETS = "max-directory-buckets";
+ public static final String MAX_BUCKETS = "max-buckets";
public static final String MAX_KEYS = "max-keys";
public static final String MAX_PARTS = "max-parts";
public static final String MAX_UPLOADS = "max-uploads";
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestRootList.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestRootList.java
index 915bb8ed372..4622564e743 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestRootList.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestRootList.java
@@ -18,10 +18,15 @@
package org.apache.hadoop.ozone.s3.endpoint;
import static org.junit.jupiter.api.Assertions.assertEquals;
+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 org.apache.hadoop.ozone.OzoneConfigKeys;
import org.apache.hadoop.ozone.client.OzoneClient;
import org.apache.hadoop.ozone.client.OzoneClientStub;
+import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -68,4 +73,93 @@ public void testListBucket() throws Exception {
assertEquals(S3Owner.DEFAULT_S3OWNER_ID, response.getOwner().getId());
}
+ @Test
+ public void testListAllBucketsPaginated() throws Exception {
+ ListBucketResponse response = listWithMaxBuckets(1);
+ assertEquals(0, response.getBucketsNum());
+ assertNull(response.getContinuationToken());
+
+ clientStub.getObjectStore().createS3Bucket("bucket-a");
+ response = listWithMaxBuckets(1);
+ assertEquals(1, response.getBucketsNum());
+ assertEquals("bucket-a", response.getBuckets().get(0).getName());
+ assertNull(response.getContinuationToken());
+
+ clientStub.getObjectStore().createS3Bucket("bucket-b");
+ response = listWithMaxBuckets(1);
+ assertEquals(1, response.getBucketsNum());
+ assertEquals("bucket-a", response.getBuckets().get(0).getName());
+ assertNotNull(response.getContinuationToken());
+
+ rootEndpoint.queryParamsForTest().set(QueryParams.CONTINUATION_TOKEN,
+ response.getContinuationToken());
+ rootEndpoint.queryParamsForTest().setInt(QueryParams.MAX_BUCKETS, 1);
+ response = (ListBucketResponse) rootEndpoint.get().getEntity();
+ assertEquals(1, response.getBucketsNum());
+ assertEquals("bucket-b", response.getBuckets().get(0).getName());
+ assertNull(response.getContinuationToken());
+ }
+
+ @Test
+ public void testListAllBucketsPaginationMultiplePages() throws Exception {
+ String bucketBaseName = "bucket-" + getClass().getName();
+ for (int i = 0; i < 5; i++) {
+ clientStub.getObjectStore().createS3Bucket(bucketBaseName + i);
+ }
+
+ ListBucketResponse response = listWithMaxBuckets(2);
+
+ assertEquals(2, response.getBucketsNum());
+ assertEquals(bucketBaseName + 0, response.getBuckets().get(0).getName());
+ assertEquals(bucketBaseName + 1, response.getBuckets().get(1).getName());
+ assertNotNull(response.getContinuationToken());
+
+ rootEndpoint.queryParamsForTest().set(QueryParams.CONTINUATION_TOKEN,
+ response.getContinuationToken());
+ response = (ListBucketResponse) rootEndpoint.get().getEntity();
+
+ assertEquals(2, response.getBucketsNum());
+ assertEquals(bucketBaseName + 2, response.getBuckets().get(0).getName());
+ assertEquals(bucketBaseName + 3, response.getBuckets().get(1).getName());
+ assertNotNull(response.getContinuationToken());
+
+ rootEndpoint.queryParamsForTest().set(QueryParams.CONTINUATION_TOKEN,
+ response.getContinuationToken());
+ response = (ListBucketResponse) rootEndpoint.get().getEntity();
+
+ assertEquals(1, response.getBucketsNum());
+ assertEquals(bucketBaseName + 4, response.getBuckets().get(0).getName());
+ assertNull(response.getContinuationToken());
+ }
+
+ @Test
+ public void testListAllBucketsInvalidMaxBuckets() {
+ rootEndpoint.queryParamsForTest().setInt(QueryParams.MAX_BUCKETS, 0);
+ assertThrows(OS3Exception.class, () -> rootEndpoint.get());
+
+ rootEndpoint.queryParamsForTest().setInt(QueryParams.MAX_BUCKETS, -1);
+ assertThrows(OS3Exception.class, () -> rootEndpoint.get());
+ }
+
+ @Test
+ public void testListAllBucketsUnpaginatedReturnsAll() throws Exception {
+ for (int i = 0; i < 3; i++) {
+ clientStub.getObjectStore().createS3Bucket("unpaginated-bucket-" + i);
+ }
+
+ rootEndpoint.queryParamsForTest().unset(QueryParams.MAX_BUCKETS);
+ rootEndpoint.queryParamsForTest().unset(QueryParams.CONTINUATION_TOKEN);
+ ListBucketResponse response =
+ (ListBucketResponse) rootEndpoint.get().getEntity();
+
+ assertEquals(3, response.getBucketsNum());
+ assertNull(response.getContinuationToken());
+ }
+
+ private ListBucketResponse listWithMaxBuckets(int maxBuckets) throws
Exception {
+ rootEndpoint.queryParamsForTest().unset(QueryParams.CONTINUATION_TOKEN);
+ rootEndpoint.queryParamsForTest().setInt(QueryParams.MAX_BUCKETS,
maxBuckets);
+ return (ListBucketResponse) rootEndpoint.get().getEntity();
+ }
+
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]