This is an automated email from the ASF dual-hosted git repository.
adoroszlai 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 037a367ceb1 HDDS-16650. Reuse bucket metadata in S3 lifecycle PUT and
DELETE (#11372)
037a367ceb1 is described below
commit 037a367ceb14c13d2f6892ab328cbbffd26145df
Author: KUAN-HAO HUANG <[email protected]>
AuthorDate: Fri Oct 2 15:50:21 2026 +0800
HDDS-16650. Reuse bucket metadata in S3 lifecycle PUT and DELETE (#11372)
---
.../ozone/s3/endpoint/BucketLifecycleHandler.java | 6 +-
.../hadoop/ozone/s3/endpoint/S3RequestContext.java | 12 ++
.../TestS3LifecycleConfigurationDelete.java | 125 +++++++++++++++++++++
.../endpoint/TestS3LifecycleConfigurationPut.java | 90 +++++++++++++++
.../ozone/s3/endpoint/TestS3RequestContext.java | 86 ++++++++++++++
5 files changed, 316 insertions(+), 3 deletions(-)
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
index c305fe99c95..a375988d660 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
@@ -93,7 +93,7 @@ public Response
deleteBucketLifecycleConfiguration(S3RequestContext context, Str
protected void deleteLifecycleConfiguration(S3RequestContext context, String
bucketName)
throws IOException, OS3Exception {
try {
- context.getVolume().getBucket(bucketName).deleteLifecycleConfiguration();
+ context.getBucket(bucketName).deleteLifecycleConfiguration();
} catch (OMException ex) {
// DeleteBucketLifecycle is idempotent: deleting a missing config
// must still return 204, not 404 — same as normal key deletion.
@@ -114,7 +114,7 @@ private void verifyBucketOwner(S3RequestContext context,
String bucketName) thro
}
try {
- String actualOwner =
context.getVolume().getBucket(bucketName).getOwner();
+ String actualOwner = context.getBucket(bucketName).getOwner();
if (actualOwner != null && !actualOwner.equals(expectedBucketOwner)) {
LOG.debug("Bucket: {}, ExpectedBucketOwner: {}, ActualBucketOwner: {}",
bucketName, expectedBucketOwner, actualOwner);
@@ -130,7 +130,7 @@ public Response
putBucketLifecycleConfiguration(S3RequestContext context, String
throws IOException, OS3Exception {
verifyBucketOwner(context, bucketName);
S3LifecycleConfiguration s3LifecycleConfiguration;
- OzoneBucket ozoneBucket = context.getVolume().getBucket(bucketName);
+ OzoneBucket ozoneBucket = context.getBucket(bucketName);
OmLifecycleConfiguration lcc;
try {
s3LifecycleConfiguration = new
PutBucketLifecycleConfigurationUnmarshaller().readFrom(body);
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
index ebcb773cef4..6fab0a75c23 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
@@ -21,8 +21,10 @@
import java.io.IOException;
import org.apache.hadoop.ozone.audit.AuditLogger.PerformanceStringBuilder;
import org.apache.hadoop.ozone.audit.S3GAction;
+import org.apache.hadoop.ozone.client.OzoneBucket;
import org.apache.hadoop.ozone.client.OzoneVolume;
import org.apache.hadoop.util.Time;
+import org.apache.ratis.util.Preconditions;
class S3RequestContext {
private final long startNanos;
@@ -30,6 +32,7 @@ class S3RequestContext {
private final EndpointBase endpoint;
private S3GAction action;
private OzoneVolume volume;
+ private OzoneBucket bucket;
S3RequestContext(EndpointBase endpoint, S3GAction action) {
this.endpoint = endpoint;
@@ -54,6 +57,15 @@ OzoneVolume getVolume() throws IOException {
return volume;
}
+ OzoneBucket getBucket(String name) throws IOException {
+ if (bucket == null) {
+ bucket = getVolume().getBucket(name);
+ } else {
+ Preconditions.assertEquals(bucket.getName(), name, "Multiple buckets in
one request are not supported");
+ }
+ return bucket;
+ }
+
S3GAction getAction() {
return action;
}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationDelete.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationDelete.java
index 5080f192b9c..0775af8fa8e 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationDelete.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationDelete.java
@@ -19,21 +19,42 @@
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse;
import static
org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_LIFECYCLE_CONFIGURATION;
+import static
org.apache.hadoop.ozone.s3.util.S3Consts.EXPECTED_BUCKET_OWNER_HEADER;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
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.OzoneClientStub;
+import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.apache.hadoop.ozone.s3.exception.S3ErrorTable;
import org.apache.hadoop.ozone.s3.util.S3Consts;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
/**
* Testing for DeleteBucketLifecycleConfiguration.
@@ -93,4 +114,108 @@ private static InputStream getBody() {
return new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
}
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = "owner")
+ public void testDeleteLifecycleLookupCount(String expectedOwner) throws
Exception {
+ ClientProtocol proxy = mock(ClientProtocol.class);
+ HttpHeaders headers = mock(HttpHeaders.class);
+
when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner);
+ BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers);
+
+ try (Response response = endpoint.delete("bucket1")) {
+ assertThat(response.getStatus()).isEqualTo(HTTP_NO_CONTENT);
+ }
+
+ verify(proxy).getBucketDetails("volume1", "bucket1");
+ verify(proxy).deleteLifecycleConfiguration("volume1", "bucket1");
+ }
+
+ @Test
+ public void testDeleteLifecycleWithoutHeaders() throws Exception {
+ ClientProtocol proxy = mock(ClientProtocol.class);
+ BucketEndpoint endpoint = newProtocolEndpoint(proxy, null);
+ try (Response response = endpoint.delete("bucket1")) {
+ assertThat(response.getStatus()).isEqualTo(HTTP_NO_CONTENT);
+ }
+ verify(proxy).deleteLifecycleConfiguration("volume1", "bucket1");
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = "owner")
+ public void testDeleteLifecycleMissingConfigurationIsIdempotent(String
expectedOwner) throws Exception {
+ ClientProtocol proxy = mock(ClientProtocol.class);
+ HttpHeaders headers = mock(HttpHeaders.class);
+
when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner);
+ BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers);
+ doThrow(new OMException("Missing configuration",
ResultCodes.LIFECYCLE_CONFIGURATION_NOT_FOUND))
+ .when(proxy).deleteLifecycleConfiguration("volume1", "bucket1");
+ try (Response response = endpoint.delete("bucket1")) {
+ assertThat(response.getStatus()).isEqualTo(HTTP_NO_CONTENT);
+ }
+ }
+
+ @ParameterizedTest
+ @CsvSource({", BUCKET_NOT_FOUND, NO_SUCH_BUCKET", "owner, BUCKET_NOT_FOUND,
NO_SUCH_BUCKET",
+ ", VOLUME_NOT_FOUND, NO_SUCH_BUCKET", "owner, VOLUME_NOT_FOUND,
NO_SUCH_BUCKET",
+ ", PERMISSION_DENIED, ACCESS_DENIED", "owner, PERMISSION_DENIED,
ACCESS_DENIED",
+ ", INTERNAL_ERROR, INTERNAL_ERROR", "owner, INTERNAL_ERROR,
INTERNAL_ERROR"})
+ public void testDeleteLifecycleProtocolErrors(
+ String expectedOwner, ResultCodes code, S3ErrorTable expected) throws
Exception {
+ ClientProtocol proxy = mock(ClientProtocol.class);
+ HttpHeaders headers = mock(HttpHeaders.class);
+
when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner);
+ BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers);
+ doThrow(new OMException("Delete failed",
code)).when(proxy).deleteLifecycleConfiguration("volume1", "bucket1");
+ assertErrorResponse(expected, () -> endpoint.delete("bucket1"));
+ }
+
+ @Test
+ public void testDeleteLifecycleRejectsOwnerBeforeDeleting() throws Exception
{
+ ClientProtocol proxy = mock(ClientProtocol.class);
+ HttpHeaders headers = mock(HttpHeaders.class);
+
when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn("other-owner");
+ BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers);
+ assertErrorResponse(S3ErrorTable.ACCESS_DENIED, () ->
endpoint.delete("bucket1"));
+ verify(proxy, never()).deleteLifecycleConfiguration("volume1", "bucket1");
+ }
+
+ @ParameterizedTest
+ @CsvSource({", BUCKET_NOT_FOUND, NO_SUCH_BUCKET", "owner, BUCKET_NOT_FOUND,
ACCESS_DENIED",
+ ", VOLUME_NOT_FOUND, NO_SUCH_BUCKET", "owner, VOLUME_NOT_FOUND,
ACCESS_DENIED",
+ ", PERMISSION_DENIED, ACCESS_DENIED", "owner, PERMISSION_DENIED,
ACCESS_DENIED",
+ ", INTERNAL_ERROR, INTERNAL_ERROR", "owner, INTERNAL_ERROR,
ACCESS_DENIED",
+ "'', PERMISSION_DENIED, ACCESS_DENIED"})
+ public void testDeleteLifecycleLookupFailurePreventsDeletion(
+ String expectedOwner, ResultCodes code, S3ErrorTable expected) throws
Exception {
+ ClientProtocol proxy = mock(ClientProtocol.class);
+ HttpHeaders headers = mock(HttpHeaders.class);
+
when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner);
+ BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers);
+ when(proxy.getBucketDetails("volume1", "bucket1")).thenThrow(new
OMException("Lookup failed", code));
+ assertErrorResponse(expected, () -> endpoint.delete("bucket1"));
+ verify(proxy, never()).deleteLifecycleConfiguration("volume1", "bucket1");
+ }
+
+ private BucketEndpoint newProtocolEndpoint(ClientProtocol proxy, HttpHeaders
requestHeaders) throws Exception {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ OzoneVolume volume = OzoneVolume.newBuilder(conf, proxy)
+ .setName("volume1").setAcls(Collections.emptyList()).build();
+ OzoneBucket bucket = OzoneBucket.newBuilder(conf, proxy)
+ .setVolumeName("volume1").setName("bucket1").setOwner("owner").build();
+ when(proxy.getBucketDetails("volume1", "bucket1")).thenReturn(bucket);
+ ObjectStore store = mock(ObjectStore.class);
+ when(store.getS3Volume()).thenReturn(volume);
+ when(store.getClientProxy()).thenReturn(proxy);
+ OzoneClient client = mock(OzoneClient.class);
+ when(client.getObjectStore()).thenReturn(store);
+ when(client.getProxy()).thenReturn(proxy);
+ BucketEndpoint endpoint = EndpointBuilder.newBucketEndpointBuilder()
+ .setClient(client).setHeaders(requestHeaders).build();
+ endpoint.queryParamsForTest().set(S3Consts.QueryParams.LIFECYCLE, "");
+ return endpoint;
+ }
+
}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
index 0891b1dd8b9..f38575bf925 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
@@ -22,6 +22,7 @@
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import static java.net.HttpURLConnection.HTTP_OK;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse;
import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.ACCESS_DENIED;
import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INTERNAL_ERROR;
import static
org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INVALID_ARGUMENT;
@@ -30,6 +31,7 @@
import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_BUCKET;
import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.QUOTA_EXCEEDED;
import static
org.apache.hadoop.ozone.s3.util.S3Consts.EXPECTED_BUCKET_OWNER_HEADER;
+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 static org.junit.jupiter.api.Assertions.assertTrue;
@@ -37,14 +39,18 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
+import java.util.Collections;
import java.util.function.Supplier;
import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.ozone.client.BucketArgs;
import org.apache.hadoop.ozone.client.ObjectStore;
import org.apache.hadoop.ozone.client.OzoneBucket;
@@ -53,14 +59,19 @@
import org.apache.hadoop.ozone.client.OzoneVolume;
import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.OmLCExpiration;
+import org.apache.hadoop.ozone.om.helpers.OmLifecycleConfiguration;
import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.apache.hadoop.ozone.s3.exception.S3ErrorTable;
import org.apache.hadoop.ozone.s3.util.S3Consts;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
/**
@@ -820,4 +831,83 @@ private static InputStream
withAbortDaysAfterInitiationOverflow() {
return new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
}
+
+ @ParameterizedTest
+ @CsvSource({", OBJECT_STORE", "'', OBJECT_STORE", "owner, OBJECT_STORE",
+ ", FILE_SYSTEM_OPTIMIZED", "owner, FILE_SYSTEM_OPTIMIZED"})
+ public void testPutLifecycleReusesBucket(String expectedOwner, BucketLayout
layout) throws Exception {
+ ClientProtocol proxy = mock(ClientProtocol.class);
+ HttpHeaders requestHeaders = mock(HttpHeaders.class);
+
when(requestHeaders.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner);
+ BucketEndpoint endpoint = newProtocolEndpoint(proxy, requestHeaders,
layout);
+
+ try (Response response = endpoint.put("bucket1", onePrefix())) {
+ assertThat(response.getStatus()).isEqualTo(HTTP_OK);
+ }
+
+ verify(proxy).getBucketDetails("volume1", "bucket1");
+ ArgumentCaptor<OmLifecycleConfiguration> config =
ArgumentCaptor.forClass(OmLifecycleConfiguration.class);
+ verify(proxy).setLifecycleConfiguration(config.capture());
+ assertThat(config.getValue().getVolume()).isEqualTo("volume1");
+ assertThat(config.getValue().getBucket()).isEqualTo("bucket1");
+ assertThat(config.getValue().getBucketLayout()).isEqualTo(layout);
+ }
+
+ @Test
+ public void testPutLifecycleOwnerMismatchPrecedesXmlValidation() throws
Exception {
+ ClientProtocol proxy = mock(ClientProtocol.class);
+ HttpHeaders requestHeaders = mock(HttpHeaders.class);
+
when(requestHeaders.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn("other-owner");
+ BucketEndpoint endpoint = newProtocolEndpoint(proxy, requestHeaders,
BucketLayout.OBJECT_STORE);
+ assertErrorResponse(ACCESS_DENIED, () -> endpoint.put("bucket1", null));
+ verify(proxy, never()).setLifecycleConfiguration(any());
+ }
+
+ @ParameterizedTest
+ @CsvSource({", BUCKET_NOT_FOUND, NO_SUCH_BUCKET", "owner, BUCKET_NOT_FOUND,
ACCESS_DENIED",
+ ", INTERNAL_ERROR, INTERNAL_ERROR", "owner, INTERNAL_ERROR,
ACCESS_DENIED",
+ ", PERMISSION_DENIED, ACCESS_DENIED", "owner, PERMISSION_DENIED,
ACCESS_DENIED",
+ "'', PERMISSION_DENIED, ACCESS_DENIED", ", VOLUME_NOT_FOUND,
NO_SUCH_BUCKET",
+ "owner, VOLUME_NOT_FOUND, ACCESS_DENIED"})
+ public void testPutLifecycleLookupFailurePrecedesXmlValidation(
+ String expectedOwner, ResultCodes code, S3ErrorTable expected) throws
Exception {
+ ClientProtocol proxy = mock(ClientProtocol.class);
+ HttpHeaders requestHeaders = mock(HttpHeaders.class);
+
when(requestHeaders.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner);
+ BucketEndpoint endpoint = newProtocolEndpoint(proxy, requestHeaders,
BucketLayout.OBJECT_STORE);
+ when(proxy.getBucketDetails("volume1", "bucket1")).thenThrow(new
OMException("Lookup failed", code));
+ assertErrorResponse(expected, () -> endpoint.put("bucket1", null));
+ verify(proxy, never()).setLifecycleConfiguration(any());
+ }
+
+ @Test
+ public void testPutLifecycleMatchingOwnerStillValidatesXml() throws
Exception {
+ ClientProtocol proxy = mock(ClientProtocol.class);
+ HttpHeaders requestHeaders = mock(HttpHeaders.class);
+
when(requestHeaders.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn("owner");
+ BucketEndpoint endpoint = newProtocolEndpoint(proxy, requestHeaders,
BucketLayout.OBJECT_STORE);
+ assertErrorResponse(MALFORMED_XML, () -> endpoint.put("bucket1", null));
+ verify(proxy, never()).setLifecycleConfiguration(any());
+ }
+
+ private BucketEndpoint newProtocolEndpoint(ClientProtocol proxy, HttpHeaders
requestHeaders, BucketLayout layout)
+ throws Exception {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ OzoneVolume volume = OzoneVolume.newBuilder(conf, proxy)
+ .setName("volume1").setAcls(Collections.emptyList()).build();
+ OzoneBucket bucket = OzoneBucket.newBuilder(conf, proxy)
+
.setVolumeName("volume1").setName("bucket1").setOwner("owner").setBucketLayout(layout).build();
+ when(proxy.getBucketDetails("volume1", "bucket1")).thenReturn(bucket);
+ ObjectStore store = mock(ObjectStore.class);
+ when(store.getS3Volume()).thenReturn(volume);
+ when(store.getClientProxy()).thenReturn(proxy);
+ OzoneClient client = mock(OzoneClient.class);
+ when(client.getObjectStore()).thenReturn(store);
+ when(client.getProxy()).thenReturn(proxy);
+ BucketEndpoint endpoint = EndpointBuilder.newBucketEndpointBuilder()
+ .setClient(client).setHeaders(requestHeaders).build();
+ endpoint.queryParamsForTest().set(S3Consts.QueryParams.LIFECYCLE, "");
+ return endpoint;
+ }
+
}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3RequestContext.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3RequestContext.java
new file mode 100644
index 00000000000..dc96fc0cde2
--- /dev/null
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3RequestContext.java
@@ -0,0 +1,86 @@
+/*
+ * 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.endpoint;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import org.apache.hadoop.ozone.audit.S3GAction;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Tests bucket reuse within a request. */
+class TestS3RequestContext {
+ private EndpointBase endpoint;
+ private OzoneVolume volume;
+ private OzoneBucket bucket;
+ private S3RequestContext context;
+
+ @BeforeEach
+ void setup() throws IOException {
+ endpoint = mock(EndpointBase.class);
+ volume = mock(OzoneVolume.class);
+ bucket = mock(OzoneBucket.class);
+ when(bucket.getName()).thenReturn("bucket1");
+ when(endpoint.getVolume()).thenReturn(volume);
+ when(volume.getBucket("bucket1")).thenReturn(bucket);
+ context = new S3RequestContext(endpoint, S3GAction.PUT_BUCKET_LIFECYCLE);
+ }
+
+ @Test
+ void reusesBucketWithinRequest() throws IOException {
+ assertThat(context.getBucket("bucket1")).isSameAs(bucket);
+ assertThat(context.getBucket("bucket1")).isSameAs(bucket);
+ verify(volume).getBucket("bucket1");
+ verify(endpoint).getVolume();
+ }
+
+ @Test
+ void rejectsDifferentBucket() throws IOException {
+ context.getBucket("bucket1");
+ assertThrows(IllegalStateException.class, () ->
context.getBucket("bucket2"));
+ verify(volume, never()).getBucket("bucket2");
+ assertThat(context.getBucket("bucket1")).isSameAs(bucket);
+ verify(volume).getBucket("bucket1");
+ }
+
+ @Test
+ void retriesFailedLookup() throws IOException {
+ when(volume.getBucket("bucket1")).thenThrow(new IOException("Lookup
failed")).thenReturn(bucket);
+ assertThrows(IOException.class, () -> context.getBucket("bucket1"));
+ assertThat(context.getBucket("bucket1")).isSameAs(bucket);
+ assertThat(context.getBucket("bucket1")).isSameAs(bucket);
+ verify(volume, times(2)).getBucket("bucket1");
+ }
+
+ @Test
+ void doesNotShareBucketBetweenRequests() throws IOException {
+ S3RequestContext second = new S3RequestContext(endpoint,
S3GAction.PUT_BUCKET_LIFECYCLE);
+ assertThat(context.getBucket("bucket1")).isSameAs(bucket);
+ assertThat(second.getBucket("bucket1")).isSameAs(bucket);
+ verify(volume, times(2)).getBucket("bucket1");
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]