This is an automated email from the ASF dual-hosted git repository.
ivandika3 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 0024c945da5 HDDS-15512. Implement S3 PUT/GET/DELETE ?tagging on
buckets (#10644)
0024c945da5 is described below
commit 0024c945da5d7574cb6bbc05f946b4721e5e998f
Author: Gargi Jaiswal <[email protected]>
AuthorDate: Fri Jul 3 17:43:53 2026 +0530
HDDS-15512. Implement S3 PUT/GET/DELETE ?tagging on buckets (#10644)
---
.../dist/src/main/smoketest/s3/buckettagging.robot | 89 +++++++++
.../dist/src/main/smoketest/s3/commonawslib.robot | 21 ++-
.../src/main/smoketest/s3/s3_compatbility_check.sh | 1 +
.../ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java | 33 ++++
.../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java | 38 ++++
.../org/apache/hadoop/ozone/audit/S3GAction.java | 3 +
.../ozone/s3/endpoint/BucketCrudHandler.java | 5 +-
.../hadoop/ozone/s3/endpoint/BucketEndpoint.java | 1 +
.../ozone/s3/endpoint/BucketTaggingHandler.java | 152 +++++++++++++++
.../hadoop/ozone/s3/endpoint/EndpointBase.java | 18 +-
.../hadoop/ozone/s3/metrics/S3GatewayMetrics.java | 90 +++++++++
.../org/apache/hadoop/ozone/s3/util/S3Consts.java | 4 +
.../ozone/s3/endpoint/EndpointTestUtils.java | 43 ++++-
.../ozone/s3/endpoint/TestBucketTaggingDelete.java | 92 +++++++++
.../ozone/s3/endpoint/TestBucketTaggingGet.java | 126 +++++++++++++
.../ozone/s3/endpoint/TestBucketTaggingPut.java | 206 +++++++++++++++++++++
.../ozone/s3/metrics/TestS3GatewayMetrics.java | 69 +++++++
17 files changed, 979 insertions(+), 12 deletions(-)
diff --git a/hadoop-ozone/dist/src/main/smoketest/s3/buckettagging.robot
b/hadoop-ozone/dist/src/main/smoketest/s3/buckettagging.robot
new file mode 100644
index 00000000000..700d50e2945
--- /dev/null
+++ b/hadoop-ozone/dist/src/main/smoketest/s3/buckettagging.robot
@@ -0,0 +1,89 @@
+# 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 bucket tagging tests with aws cli
+Library OperatingSystem
+Library String
+Resource ../commonlib.robot
+Resource commonawslib.robot
+Test Timeout 5 minutes
+Suite Setup Setup bucket tagging tests
+
+*** Variables ***
+${ENDPOINT_URL} http://s3g:9878
+${OZONE_TEST} true
+${BUCKET} generated
+${LINK_BUCKET} link-bucket-tagging
+
+*** Keywords ***
+Setup bucket tagging tests
+ Setup s3 tests
+ Setup link bucket for tagging ${LINK_BUCKET}
+
+*** Test Cases ***
+
+Get bucket tagging without tags
+ ${result} = Execute AWSS3APICli and checkrc get-bucket-tagging
--bucket ${BUCKET} 255
+ Should contain ${result}
NoSuchTagSet
+
+Put bucket tagging
+ Execute AWSS3ApiCli put-bucket-tagging
--bucket ${BUCKET} --tagging '{"TagSet": [{ "Key": "tag-key1", "Value":
"tag-value1" }]}'
+
+Get bucket tagging
+ ${result} = Execute AWSS3ApiCli get-bucket-tagging
--bucket ${BUCKET}
+ Should contain ${result}
TagSet
+ ${tagCount} = Execute and checkrc echo '${result}' | jq
'.TagSet | length' 0
+ Should Be Equal ${tagCount}
1
+
+Put bucket tagging overwrites existing tags
+ Execute AWSS3ApiCli put-bucket-tagging
--bucket ${BUCKET} --tagging '{"TagSet": [{ "Key": "tag-key2", "Value":
"tag-value2" },{ "Key": "tag-key3", "Value": "tag-value3" }]}'
+
+Get bucket tagging after overwrite
+ ${result} = Execute AWSS3ApiCli get-bucket-tagging
--bucket ${BUCKET}
+ Should contain ${result}
TagSet
+ ${tagCount} = Execute and checkrc echo '${result}' | jq
'.TagSet | length' 0
+ Should Be Equal ${tagCount}
2
+
+Put bucket tagging on nonexistent bucket
+ ${result} = Execute AWSS3APICli and checkrc put-bucket-tagging
--bucket ${PREFIX}-missing-bucket-tagging --tagging '{"TagSet": [{ "Key":
"tag-key1", "Value": "tag-value1" }]}' 255
+ Should contain ${result}
NoSuchBucket
+
+Delete bucket tagging
+ Execute AWSS3ApiCli delete-bucket-tagging --bucket
${BUCKET}
+
+Get bucket tagging after delete returns NoSuchTagSet
+ ${result} = Execute AWSS3APICli and checkrc get-bucket-tagging
--bucket ${BUCKET} 255
+ Should contain ${result}
NoSuchTagSet
+
+Get bucket tagging on link bucket without tags
+ ${result} = Execute AWSS3APICli and checkrc get-bucket-tagging
--bucket ${LINK_BUCKET} 255
+ Should contain ${result}
NoSuchTagSet
+
+Put bucket tagging on link bucket
+ Execute AWSS3ApiCli put-bucket-tagging
--bucket ${LINK_BUCKET} --tagging '{"TagSet": [{ "Key": "tag-key1", "Value":
"tag-value1" }]}'
+
+Get bucket tagging on link bucket
+ ${result} = Execute AWSS3ApiCli get-bucket-tagging
--bucket ${LINK_BUCKET}
+ Should contain ${result}
TagSet
+ ${tagCount} = Execute and checkrc echo '${result}' | jq
'.TagSet | length' 0
+ Should Be Equal ${tagCount}
1
+
+Delete bucket tagging on link bucket
+ Execute AWSS3ApiCli delete-bucket-tagging
--bucket ${LINK_BUCKET}
+
+Get bucket tagging on link bucket after delete
+ ${result} = Execute AWSS3APICli and checkrc get-bucket-tagging
--bucket ${LINK_BUCKET} 255
+ Should contain ${result}
NoSuchTagSet
diff --git a/hadoop-ozone/dist/src/main/smoketest/s3/commonawslib.robot
b/hadoop-ozone/dist/src/main/smoketest/s3/commonawslib.robot
index 09f86e6b537..16bf579f4df 100644
--- a/hadoop-ozone/dist/src/main/smoketest/s3/commonawslib.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/s3/commonawslib.robot
@@ -143,12 +143,25 @@ Setup s3 tests
Set Global Variable ${OZONE_S3_TESTS_SET_UP} ${TRUE}
Setup links for S3 tests
- ${exists} = Bucket Exists o3://${OM_SERVICE_ID}/s3v/link
+ ${exists} = Bucket Exists s3v/link
Return From Keyword If ${exists}
- Execute ozone sh volume create o3://${OM_SERVICE_ID}/legacy
- Execute ozone sh bucket create --layout ${BUCKET_LAYOUT}
o3://${OM_SERVICE_ID}/legacy/source-bucket
+ Ensure legacy source bucket
Create link link
+Ensure legacy source bucket
+ ${source_exists} = Bucket Exists legacy/source-bucket
+ Return From Keyword If ${source_exists}
+ ${rc} ${output} = Run And Return Rc And Output ozone sh volume
create legacy
+ Run Keyword If ${rc} != 0 Should Contain ${output}
VOLUME_ALREADY_EXISTS
+ Execute ozone sh bucket create --layout ${BUCKET_LAYOUT}
legacy/source-bucket
+
+Setup link bucket for tagging
+ [Arguments] ${link_bucket_name}=link-bucket-tagging
+ ${exists} = Bucket Exists s3v/${link_bucket_name}
+ Return From Keyword If ${exists}
+ Ensure legacy source bucket
+ Create link ${link_bucket_name}
+
Create generated bucket
[Arguments] ${layout}=OBJECT_STORE
${BUCKET} = Create bucket with layout s3v ${layout}
@@ -162,7 +175,7 @@ Create encrypted bucket
Create link
[arguments] ${bucket}
- Execute ozone sh bucket link
o3://${OM_SERVICE_ID}/legacy/source-bucket o3://${OM_SERVICE_ID}/s3v/${bucket}
+ Execute ozone sh bucket link legacy/source-bucket s3v/${bucket}
[return] ${bucket}
Create EC bucket
diff --git a/hadoop-ozone/dist/src/main/smoketest/s3/s3_compatbility_check.sh
b/hadoop-ozone/dist/src/main/smoketest/s3/s3_compatbility_check.sh
index 8b6a4f5a55a..475075fdab6 100755
--- a/hadoop-ozone/dist/src/main/smoketest/s3/s3_compatbility_check.sh
+++ b/hadoop-ozone/dist/src/main/smoketest/s3/s3_compatbility_check.sh
@@ -85,6 +85,7 @@ run_robot_test objectmultidelete
run_robot_test objecthead
run_robot_test MultipartUpload
run_robot_test objecttagging
+run_robot_test buckettagging
run_robot_test objectlist
rebot --outputdir results/ results/*.xml
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 e810a8786b9..c31e3154c32 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
@@ -39,13 +39,16 @@
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.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.CreateBucketRequest;
+import com.amazonaws.services.s3.model.DeleteBucketTaggingConfigurationRequest;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
+import com.amazonaws.services.s3.model.GetBucketTaggingConfigurationRequest;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.GetObjectTaggingRequest;
import com.amazonaws.services.s3.model.GetObjectTaggingResult;
@@ -72,9 +75,11 @@
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.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;
@@ -1265,6 +1270,34 @@ public void testGetObjectTaggingReturnsTagsSortedByKey()
{
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<String, String> 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
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 d807be57483..90c850abff5 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
@@ -116,10 +116,13 @@
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse;
import software.amazon.awssdk.services.s3.model.Delete;
import software.amazon.awssdk.services.s3.model.DeleteBucketRequest;
+import software.amazon.awssdk.services.s3.model.DeleteBucketTaggingRequest;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.DeleteObjectTaggingRequest;
import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest;
import software.amazon.awssdk.services.s3.model.GetBucketAclRequest;
+import software.amazon.awssdk.services.s3.model.GetBucketTaggingRequest;
+import software.amazon.awssdk.services.s3.model.GetBucketTaggingResponse;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.GetObjectTaggingRequest;
@@ -139,6 +142,7 @@
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
import software.amazon.awssdk.services.s3.model.PutBucketAclRequest;
+import software.amazon.awssdk.services.s3.model.PutBucketTaggingRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
import software.amazon.awssdk.services.s3.model.PutObjectTaggingRequest;
@@ -305,6 +309,40 @@ public void testGetObjectTaggingReturnsTagsSortedByKey() {
assertEquals("val2", tagSet.get(1).value());
}
+ @Test
+ public void testBucketTaggingPutGetDelete() {
+ final String bucketName = getBucketName();
+ s3Client.createBucket(b -> b.bucket(bucketName));
+
+ S3Exception noTags = assertThrows(S3Exception.class,
+ () ->
s3Client.getBucketTagging(GetBucketTaggingRequest.builder().bucket(bucketName).build()));
+ assertEquals(404, noTags.statusCode());
+ assertEquals("NoSuchTagSet", noTags.awsErrorDetails().errorCode());
+
+ List<Tag> tags = Arrays.asList(
+ Tag.builder().key("tag-key1").value("tag-value1").build(),
+ Tag.builder().key("tag-key2").value("tag-value2").build());
+ s3Client.putBucketTagging(PutBucketTaggingRequest.builder()
+ .bucket(bucketName)
+ .tagging(Tagging.builder().tagSet(tags).build())
+ .build());
+
+ GetBucketTaggingResponse taggingResult = s3Client.getBucketTagging(
+ GetBucketTaggingRequest.builder().bucket(bucketName).build());
+ Map<String, String> actualTags = taggingResult.tagSet().stream()
+ .collect(Collectors.toMap(Tag::key, Tag::value));
+ assertEquals(2, actualTags.size());
+ assertEquals("tag-value1", actualTags.get("tag-key1"));
+ assertEquals("tag-value2", actualTags.get("tag-key2"));
+
+
s3Client.deleteBucketTagging(DeleteBucketTaggingRequest.builder().bucket(bucketName).build());
+
+ S3Exception afterDelete = assertThrows(S3Exception.class,
+ () ->
s3Client.getBucketTagging(GetBucketTaggingRequest.builder().bucket(bucketName).build()));
+ assertEquals(404, afterDelete.statusCode());
+ assertEquals("NoSuchTagSet", afterDelete.awsErrorDetails().errorCode());
+ }
+
@Test
public void testPutObjectIfNoneMatch() {
final String bucketName = getBucketName();
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
index 969b2c04809..0ab5a1004ba 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
@@ -55,6 +55,9 @@ public enum S3GAction implements AuditAction {
GET_OBJECT_TAGGING,
PUT_OBJECT_TAGGING,
DELETE_OBJECT_TAGGING,
+ GET_BUCKET_TAGGING,
+ PUT_BUCKET_TAGGING,
+ DELETE_BUCKET_TAGGING,
PUT_OBJECT_ACL;
@Override
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketCrudHandler.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketCrudHandler.java
index a2698b3098e..41a19231a82 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketCrudHandler.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketCrudHandler.java
@@ -34,7 +34,7 @@
*
* This handler processes bucket-level requests that do not target
* specific subresources (such as {@code ?acl}, {@code ?uploads},
- * or {@code ?delete}), which are handled by dedicated handlers.
+ * {@code ?delete} or {@code ?tagging}), which are handled by dedicated
handlers.
*
* This handler extends EndpointBase to inherit all required functionality
* (configuration, headers, request context, audit logging, metrics, etc.).
@@ -47,7 +47,8 @@ public class BucketCrudHandler extends BucketOperationHandler
{
private boolean shouldHandle() {
return queryParams().get(QueryParams.ACL) == null
&& queryParams().get(QueryParams.UPLOADS) == null
- && queryParams().get(QueryParams.DELETE) == null;
+ && queryParams().get(QueryParams.DELETE) == null
+ && queryParams().get(QueryParams.TAGGING) == null;
}
/**
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
index d35da257cd0..383dc753f19 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
@@ -424,6 +424,7 @@ protected void init() {
.add(new BucketGetLocationHandler())
.add(new BucketAclHandler())
.add(new ListMultipartUploadsHandler())
+ .add(new BucketTaggingHandler())
.add(new BucketCrudHandler())
.add(this)
.build();
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketTaggingHandler.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketTaggingHandler.java
new file mode 100644
index 00000000000..10d41558ec9
--- /dev/null
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketTaggingHandler.java
@@ -0,0 +1,152 @@
+/*
+ * 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.apache.hadoop.ozone.s3.util.S3Consts.TAG_BUCKET_NUM_LIMIT;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.function.Supplier;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.hadoop.ozone.audit.S3GAction;
+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.apache.ratis.util.MemoizedSupplier;
+
+/**
+ * S3 bucket tagging (?tagging).
+ */
+public class BucketTaggingHandler extends BucketOperationHandler {
+
+ private static final Supplier<MessageUnmarshaller<S3Tagging>> UNMARSHALLER =
+ MemoizedSupplier.valueOf(() -> new
MessageUnmarshaller<>(S3Tagging.class));
+
+ @Override
+ public Response handlePutRequest(S3RequestContext context, String
bucketName, InputStream body)
+ throws IOException, OS3Exception {
+ S3GAction action = resolveAction();
+ if (action == null) {
+ return null;
+ }
+ context.setAction(action);
+
+ try {
+ S3Tagging tagging;
+ try {
+ tagging = UNMARSHALLER.get().readFrom(body);
+ tagging.validate();
+ } catch (Exception ex) {
+ OS3Exception exception =
S3ErrorTable.newError(S3ErrorTable.MALFORMED_XML, bucketName, ex);
+ exception.setErrorMessage(exception.getErrorMessage() + ". " +
ex.getMessage());
+ throw exception;
+ }
+
+ Map<String, String> tags = validateAndGetTagging(
+ tagging.getTagSet().getTags(),
+ S3Tagging.Tag::getKey,
+ S3Tagging.Tag::getValue,
+ TAG_BUCKET_NUM_LIMIT
+ );
+
+ context.getVolume().getBucket(bucketName).putBucketTagging(tags);
+
+ getMetrics().updatePutBucketTaggingSuccessStats(context.getStartNanos());
+
+ return Response.ok().build();
+ } catch (Exception e) {
+ getMetrics().updatePutBucketTaggingFailureStats(context.getStartNanos());
+ throw e;
+ }
+ }
+
+ @Override
+ public Response handleDeleteRequest(S3RequestContext context, String
bucketName)
+ throws IOException, OS3Exception {
+ S3GAction action = resolveAction();
+ if (action == null) {
+ return null;
+ }
+ context.setAction(action);
+
+ try {
+ context.getVolume().getBucket(bucketName).deleteBucketTagging();
+
getMetrics().updateDeleteBucketTaggingSuccessStats(context.getStartNanos());
+ return Response.noContent().build();
+ } catch (OMException ex) {
+
getMetrics().updateDeleteBucketTaggingFailureStats(context.getStartNanos());
+ if (ex.getResult() == ResultCodes.BUCKET_NOT_FOUND) {
+ throw S3ErrorTable.newError(S3ErrorTable.NO_SUCH_BUCKET, bucketName);
+ }
+ throw ex;
+ } catch (IOException | RuntimeException ex) {
+
getMetrics().updateDeleteBucketTaggingFailureStats(context.getStartNanos());
+ throw ex;
+ }
+ }
+
+ @Override
+ public Response handleGetRequest(S3RequestContext context, String bucketName)
+ throws IOException, OS3Exception {
+ S3GAction action = resolveAction();
+ if (action == null) {
+ return null;
+ }
+ context.setAction(action);
+
+ try {
+ Map<String, String> tagMap =
context.getVolume().getBucket(bucketName).getBucketTagging();
+ if (tagMap.isEmpty()) {
+ throw S3ErrorTable.newError(S3ErrorTable.NO_SUCH_TAG_SET, bucketName);
+ }
+ getMetrics().updateGetBucketTaggingSuccessStats(context.getStartNanos());
+ return Response.ok(S3Tagging.fromMap(tagMap),
MediaType.APPLICATION_XML_TYPE).build();
+ } catch (OMException ex) {
+ getMetrics().updateGetBucketTaggingFailureStats(context.getStartNanos());
+ if (ex.getResult() == ResultCodes.BUCKET_NOT_FOUND) {
+ throw S3ErrorTable.newError(S3ErrorTable.NO_SUCH_BUCKET, bucketName);
+ }
+ throw ex;
+ } catch (Exception e) {
+ getMetrics().updateGetBucketTaggingFailureStats(context.getStartNanos());
+ throw e;
+ }
+ }
+
+ private S3GAction resolveAction() {
+ if (queryParams().get(S3Consts.QueryParams.TAGGING) == null) {
+ return null;
+ }
+
+ switch (getContext().getMethod()) {
+ case HttpMethod.DELETE:
+ return S3GAction.DELETE_BUCKET_TAGGING;
+ case HttpMethod.GET:
+ return S3GAction.GET_BUCKET_TAGGING;
+ case HttpMethod.PUT:
+ return S3GAction.PUT_BUCKET_TAGGING;
+ default:
+ return null;
+ }
+ }
+}
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
index 052dd34faa5..452303dc4c0 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
@@ -403,6 +403,15 @@ protected static <KV> Map<String, String>
validateAndGetTagging(
List<KV> tagList,
Function<KV, String> getTagKey,
Function<KV, String> getTagValue
+ ) throws OS3Exception {
+ return validateAndGetTagging(tagList, getTagKey, getTagValue,
TAG_NUM_LIMIT);
+ }
+
+ protected static <KV> Map<String, String> validateAndGetTagging(
+ List<KV> tagList,
+ Function<KV, String> getTagKey,
+ Function<KV, String> getTagValue,
+ int maxTagCount
) throws OS3Exception {
final Map<String, String> tags = new HashMap<>();
for (KV tagPair : tagList) {
@@ -455,19 +464,18 @@ protected static <KV> Map<String, String>
validateAndGetTagging(
final String previous = tags.put(tagKey, tagValue);
if (previous != null) {
- // Tags that are associated with an object must have unique tag keys
- // Reject request if the same key is used twice on the same resource
+ // Tags must have unique keys on the same resource (object or bucket).
OS3Exception ex = S3ErrorTable.newError(INVALID_TAG, tagKey);
ex.setErrorMessage("There are tags with duplicate tag keys, tag keys
should be unique");
throw ex;
}
}
- if (tags.size() > TAG_NUM_LIMIT) {
- // You can associate up to 10 tags with an object.
+ if (tags.size() > maxTagCount) {
+ // You can associate up to 10 tags with an object and up to 50 tags with
a bucket.
OS3Exception ex = S3ErrorTable.newError(INVALID_TAG, TAG_HEADER);
ex.setErrorMessage("The number of tags " + tags.size() +
- " exceeded the maximum number of tags of " + TAG_NUM_LIMIT);
+ " exceeded the maximum number of tags of " + maxTagCount);
throw ex;
}
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/metrics/S3GatewayMetrics.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/metrics/S3GatewayMetrics.java
index 7f2b2f67863..160102e7290 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/metrics/S3GatewayMetrics.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/metrics/S3GatewayMetrics.java
@@ -101,6 +101,12 @@ public final class S3GatewayMetrics implements Closeable,
MetricsSource {
private @Metric MutableCounterLong putObjectTaggingFailure;
private @Metric MutableCounterLong deleteObjectTaggingSuccess;
private @Metric MutableCounterLong deleteObjectTaggingFailure;
+ private @Metric MutableCounterLong getBucketTaggingSuccess;
+ private @Metric MutableCounterLong getBucketTaggingFailure;
+ private @Metric MutableCounterLong putBucketTaggingSuccess;
+ private @Metric MutableCounterLong putBucketTaggingFailure;
+ private @Metric MutableCounterLong deleteBucketTaggingSuccess;
+ private @Metric MutableCounterLong deleteBucketTaggingFailure;
private @Metric MutableCounterLong putObjectAclSuccess;
private @Metric MutableCounterLong putObjectAclFailure;
@@ -272,6 +278,24 @@ public final class S3GatewayMetrics implements Closeable,
MetricsSource {
@Metric(about = "Latency for failing to delete object tagging of a key in
nanoseconds")
private PerformanceMetrics deleteObjectTaggingFailureLatencyNs;
+ @Metric(about = "Latency for successful get bucket tagging in nanoseconds")
+ private PerformanceMetrics getBucketTaggingSuccessLatencyNs;
+
+ @Metric(about = "Latency for failing to get bucket tagging in nanoseconds")
+ private PerformanceMetrics getBucketTaggingFailureLatencyNs;
+
+ @Metric(about = "Latency for successful put bucket tagging in nanoseconds")
+ private PerformanceMetrics putBucketTaggingSuccessLatencyNs;
+
+ @Metric(about = "Latency for failing to put bucket tagging in nanoseconds")
+ private PerformanceMetrics putBucketTaggingFailureLatencyNs;
+
+ @Metric(about = "Latency for successful delete bucket tagging in
nanoseconds")
+ private PerformanceMetrics deleteBucketTaggingSuccessLatencyNs;
+
+ @Metric(about = "Latency for failing to delete bucket tagging in
nanoseconds")
+ private PerformanceMetrics deleteBucketTaggingFailureLatencyNs;
+
@Metric(about = "Latency for successfully setting an S3 object ACL " +
"in nanoseconds")
private PerformanceMetrics putObjectAclSuccessLatencyNs;
@@ -427,6 +451,18 @@ public void getMetrics(MetricsCollector collector, boolean
all) {
deleteObjectTaggingSuccessLatencyNs.snapshot(recordBuilder, true);
deleteObjectTaggingFailure.snapshot(recordBuilder, true);
deleteObjectTaggingFailureLatencyNs.snapshot(recordBuilder, true);
+ getBucketTaggingSuccess.snapshot(recordBuilder, true);
+ getBucketTaggingSuccessLatencyNs.snapshot(recordBuilder, true);
+ getBucketTaggingFailure.snapshot(recordBuilder, true);
+ getBucketTaggingFailureLatencyNs.snapshot(recordBuilder, true);
+ putBucketTaggingSuccess.snapshot(recordBuilder, true);
+ putBucketTaggingSuccessLatencyNs.snapshot(recordBuilder, true);
+ putBucketTaggingFailure.snapshot(recordBuilder, true);
+ putBucketTaggingFailureLatencyNs.snapshot(recordBuilder, true);
+ deleteBucketTaggingSuccess.snapshot(recordBuilder, true);
+ deleteBucketTaggingSuccessLatencyNs.snapshot(recordBuilder, true);
+ deleteBucketTaggingFailure.snapshot(recordBuilder, true);
+ deleteBucketTaggingFailureLatencyNs.snapshot(recordBuilder, true);
putObjectAclSuccess.snapshot(recordBuilder, true);
putObjectAclSuccessLatencyNs.snapshot(recordBuilder, true);
putObjectAclFailure.snapshot(recordBuilder, true);
@@ -682,6 +718,36 @@ public void updateDeleteObjectTaggingFailureStats(long
startNanos) {
this.deleteObjectTaggingFailureLatencyNs.add(Time.monotonicNowNanos() -
startNanos);
}
+ public void updateGetBucketTaggingSuccessStats(long startNanos) {
+ this.getBucketTaggingSuccess.incr();
+ this.getBucketTaggingSuccessLatencyNs.add(Time.monotonicNowNanos() -
startNanos);
+ }
+
+ public void updateGetBucketTaggingFailureStats(long startNanos) {
+ this.getBucketTaggingFailure.incr();
+ this.getBucketTaggingFailureLatencyNs.add(Time.monotonicNowNanos() -
startNanos);
+ }
+
+ public void updatePutBucketTaggingSuccessStats(long startNanos) {
+ this.putBucketTaggingSuccess.incr();
+ this.putBucketTaggingSuccessLatencyNs.add(Time.monotonicNowNanos() -
startNanos);
+ }
+
+ public void updatePutBucketTaggingFailureStats(long startNanos) {
+ this.putBucketTaggingFailure.incr();
+ this.putBucketTaggingFailureLatencyNs.add(Time.monotonicNowNanos() -
startNanos);
+ }
+
+ public void updateDeleteBucketTaggingSuccessStats(long startNanos) {
+ this.deleteBucketTaggingSuccess.incr();
+ this.deleteBucketTaggingSuccessLatencyNs.add(Time.monotonicNowNanos() -
startNanos);
+ }
+
+ public void updateDeleteBucketTaggingFailureStats(long startNanos) {
+ this.deleteBucketTaggingFailure.incr();
+ this.deleteBucketTaggingFailureLatencyNs.add(Time.monotonicNowNanos() -
startNanos);
+ }
+
public void updatePutObjectAclSuccessStats(long startNanos) {
this.putObjectAclSuccess.incr();
this.putObjectAclSuccessLatencyNs.add(Time.monotonicNowNanos() -
startNanos);
@@ -857,6 +923,30 @@ public long getDeleteObjectTaggingFailure() {
return deleteObjectTaggingFailure.value();
}
+ public long getGetBucketTaggingSuccess() {
+ return getBucketTaggingSuccess.value();
+ }
+
+ public long getGetBucketTaggingFailure() {
+ return getBucketTaggingFailure.value();
+ }
+
+ public long getPutBucketTaggingSuccess() {
+ return putBucketTaggingSuccess.value();
+ }
+
+ public long getPutBucketTaggingFailure() {
+ return putBucketTaggingFailure.value();
+ }
+
+ public long getDeleteBucketTaggingSuccess() {
+ return deleteBucketTaggingSuccess.value();
+ }
+
+ public long getDeleteBucketTaggingFailure() {
+ return deleteBucketTaggingFailure.value();
+ }
+
private long updateAndGetStats(PerformanceMetrics metric, long startNanos) {
long value = Time.monotonicNowNanos() - startNanos;
metric.add(value);
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 abc654f916e..92266adbcab 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
@@ -87,6 +87,7 @@ public final class S3Consts {
public static final String TAG_COUNT_HEADER = "x-amz-tagging-count";
public static final String AWS_TAG_PREFIX = "aws:";
+ // tag limit for object
public static final int TAG_NUM_LIMIT = 10;
public static final int TAG_KEY_LENGTH_LIMIT = 128;
public static final int TAG_VALUE_LENGTH_LIMIT = 256;
@@ -117,6 +118,9 @@ public final class S3Consts {
public static final String DEFAULT_S3_REGION = "us-east-1";
public static final String S3_EXPRESS_SERVICE = "s3express";
+ // tag limit for bucket
+ public static final int TAG_BUCKET_NUM_LIMIT = 50;
+
//Never Constructed
private S3Consts() {
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/EndpointTestUtils.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/EndpointTestUtils.java
index 8827150069f..1e48f6c9be9 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/EndpointTestUtils.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/EndpointTestUtils.java
@@ -177,6 +177,47 @@ public static Response deleteTagging(
return subject.delete(bucket, key);
}
+ /**
+ * Get bucket tags (?tagging).
+ */
+ public static Response getBucketTagging(
+ BucketEndpoint subject,
+ String bucket
+ ) throws IOException, OS3Exception {
+ subject.queryParamsForTest().set(S3Consts.QueryParams.TAGGING, "");
+ when(subject.getContext().getMethod()).thenReturn(HttpMethod.GET);
+ return subject.get(bucket);
+ }
+
+ /**
+ * Add tagging on bucket (?tagging).
+ */
+ public static Response putBucketTagging(
+ BucketEndpoint subject, String bucket, String content)
+ throws IOException, OS3Exception {
+ subject.queryParamsForTest().set(S3Consts.QueryParams.TAGGING, "");
+ when(subject.getContext().getMethod()).thenReturn(HttpMethod.PUT);
+ setLengthHeader(subject, content);
+
+ if (content == null) {
+ return subject.put(bucket, null);
+ } else {
+ try (ByteArrayInputStream body = new
ByteArrayInputStream(content.getBytes(UTF_8))) {
+ return subject.put(bucket, body);
+ }
+ }
+ }
+
+ /**
+ * Delete bucket tags (?tagging).
+ */
+ public static Response deleteBucketTagging(
+ BucketEndpoint subject, String bucket) throws IOException, OS3Exception {
+ subject.queryParamsForTest().set(S3Consts.QueryParams.TAGGING, "");
+ when(subject.getContext().getMethod()).thenReturn(HttpMethod.DELETE);
+ return subject.delete(bucket);
+ }
+
/** Initiate multipart upload.
* @return upload ID */
public static String initiateMultipartUpload(ObjectEndpoint subject, String
bucket, String key)
@@ -279,7 +320,7 @@ public static OS3Exception assertErrorResponse(S3ErrorTable
expected, CheckedSup
return actual;
}
- private static void setLengthHeader(ObjectEndpoint subject, String content) {
+ private static void setLengthHeader(EndpointBase subject, String content) {
when(subject.getHeaders().getHeaderString(HttpHeaders.CONTENT_LENGTH))
.thenReturn(String.valueOf(contentLength(content)));
}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketTaggingDelete.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketTaggingDelete.java
new file mode 100644
index 00000000000..b8ad60a245e
--- /dev/null
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketTaggingDelete.java
@@ -0,0 +1,92 @@
+/*
+ * 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 java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertSucceeds;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.deleteBucketTagging;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.putBucketTagging;
+import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_BUCKET;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.STORAGE_CLASS_HEADER;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.X_AMZ_CONTENT_SHA256;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+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;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+/**
+ * Tests for DeleteBucketTagging.
+ */
+public class TestBucketTaggingDelete {
+
+ private static final String BUCKET_NAME = "b1";
+ private BucketEndpoint rest;
+ private OzoneClient client;
+
+ private static String twoTagsBody() {
+ return
+ "<Tagging xmlns=\"" + S3Consts.S3_XML_NAMESPACE + "\">" +
+ " <TagSet>" +
+ " <Tag><Key>tag1</Key><Value>value1</Value></Tag>" +
+ " <Tag><Key>tag2</Key><Value>value2</Value></Tag>" +
+ " </TagSet>" +
+ "</Tagging>";
+ }
+
+ @BeforeEach
+ public void init() throws OS3Exception, IOException {
+ client = new OzoneClientStub();
+ client.getObjectStore().createS3Bucket(BUCKET_NAME);
+
+ HttpHeaders headers = Mockito.mock(HttpHeaders.class);
+ Mockito.when(headers.getHeaderString(X_AMZ_CONTENT_SHA256))
+ .thenReturn("UNSIGNED-PAYLOAD");
+ Mockito.when(headers.getHeaderString(STORAGE_CLASS_HEADER))
+ .thenReturn("STANDARD");
+
+ rest = EndpointBuilder.newBucketEndpointBuilder()
+ .setClient(client)
+ .setHeaders(headers)
+ .build();
+
+ assertSucceeds(() -> putBucketTagging(rest, BUCKET_NAME, twoTagsBody()));
+ }
+
+ @Test
+ public void testDeleteBucketTagging() throws IOException, OS3Exception {
+ Response response = deleteBucketTagging(rest, BUCKET_NAME);
+ assertEquals(HTTP_NO_CONTENT, response.getStatus());
+
+
assertTrue(client.getObjectStore().getS3Bucket(BUCKET_NAME).getBucketTagging().isEmpty());
+ }
+
+ @Test
+ public void testDeleteBucketTaggingNoBucketFound() {
+ assertErrorResponse(NO_SUCH_BUCKET, () -> deleteBucketTagging(rest,
"nonexistent"));
+ }
+}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketTaggingGet.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketTaggingGet.java
new file mode 100644
index 00000000000..d685e2c7ebf
--- /dev/null
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketTaggingGet.java
@@ -0,0 +1,126 @@
+/*
+ * 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 java.net.HttpURLConnection.HTTP_OK;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertSucceeds;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.getBucketTagging;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.putBucketTagging;
+import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_BUCKET;
+import static
org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_TAG_SET;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.STORAGE_CLASS_HEADER;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.X_AMZ_CONTENT_SHA256;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneClientStub;
+import org.apache.hadoop.ozone.s3.endpoint.S3Tagging.Tag;
+import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.apache.hadoop.ozone.s3.util.S3Consts;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+/**
+ * Tests for GetBucketTagging.
+ */
+public class TestBucketTaggingGet {
+
+ private static final String BUCKET_NAME = "b1";
+ private BucketEndpoint rest;
+
+ private static String twoTagsBody() {
+ return
+ "<Tagging xmlns=\"" + S3Consts.S3_XML_NAMESPACE + "\">" +
+ " <TagSet>" +
+ " <Tag><Key>tag1</Key><Value>value1</Value></Tag>" +
+ " <Tag><Key>tag2</Key><Value>value2</Value></Tag>" +
+ " </TagSet>" +
+ "</Tagging>";
+ }
+
+ @BeforeEach
+ public void init() throws Exception {
+ OzoneClient client = new OzoneClientStub();
+ client.getObjectStore().createS3Bucket(BUCKET_NAME);
+
+ HttpHeaders headers = Mockito.mock(HttpHeaders.class);
+ Mockito.when(headers.getHeaderString(X_AMZ_CONTENT_SHA256))
+ .thenReturn("UNSIGNED-PAYLOAD");
+ Mockito.when(headers.getHeaderString(STORAGE_CLASS_HEADER))
+ .thenReturn("STANDARD");
+
+ rest = EndpointBuilder.newBucketEndpointBuilder()
+ .setClient(client)
+ .setHeaders(headers)
+ .build();
+
+ assertSucceeds(() -> putBucketTagging(rest, BUCKET_NAME, twoTagsBody()));
+ }
+
+ @Test
+ public void testGetBucketTagging() throws IOException, OS3Exception {
+ Response response = getBucketTagging(rest, BUCKET_NAME);
+
+ assertEquals(HTTP_OK, response.getStatus());
+ S3Tagging s3Tagging = (S3Tagging) response.getEntity();
+ assertNotNull(s3Tagging);
+ assertNotNull(s3Tagging.getTagSet());
+ assertEquals(2, s3Tagging.getTagSet().getTags().size());
+ for (Tag tag : s3Tagging.getTagSet().getTags()) {
+ if (tag.getKey().equals("tag1")) {
+ assertEquals("value1", tag.getValue());
+ } else if (tag.getKey().equals("tag2")) {
+ assertEquals("value2", tag.getValue());
+ } else {
+ fail("Unknown tag found");
+ }
+ }
+ }
+
+ @Test
+ public void testGetBucketTaggingNoTagSet() throws Exception {
+ OzoneClient client = new OzoneClientStub();
+ String emptyBucket = "empty-tags-bucket";
+ client.getObjectStore().createS3Bucket(emptyBucket);
+
+ HttpHeaders headers = Mockito.mock(HttpHeaders.class);
+ Mockito.when(headers.getHeaderString(X_AMZ_CONTENT_SHA256))
+ .thenReturn("UNSIGNED-PAYLOAD");
+ Mockito.when(headers.getHeaderString(STORAGE_CLASS_HEADER))
+ .thenReturn("STANDARD");
+
+ BucketEndpoint endpoint = EndpointBuilder.newBucketEndpointBuilder()
+ .setClient(client)
+ .setHeaders(headers)
+ .build();
+
+ assertErrorResponse(NO_SUCH_TAG_SET, () -> getBucketTagging(endpoint,
emptyBucket));
+ }
+
+ @Test
+ public void testGetBucketTaggingNoBucketFound() {
+ assertErrorResponse(NO_SUCH_BUCKET, () -> getBucketTagging(rest,
"nonexistent"));
+ }
+}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketTaggingPut.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketTaggingPut.java
new file mode 100644
index 00000000000..6d19c39f20d
--- /dev/null
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketTaggingPut.java
@@ -0,0 +1,206 @@
+/*
+ * 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.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertSucceeds;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.putBucketTagging;
+import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INVALID_TAG;
+import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.MALFORMED_XML;
+import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_BUCKET;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.STORAGE_CLASS_HEADER;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.TAG_BUCKET_NUM_LIMIT;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.TAG_KEY_LENGTH_LIMIT;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.TAG_VALUE_LENGTH_LIMIT;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.X_AMZ_CONTENT_SHA256;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.util.Map;
+import javax.ws.rs.core.HttpHeaders;
+import org.apache.commons.lang3.StringUtils;
+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;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for PutBucketTagging.
+ */
+public class TestBucketTaggingPut {
+
+ private static final String BUCKET_NAME = "b1";
+ private BucketEndpoint rest;
+
+ @BeforeEach
+ public void init() throws OS3Exception, IOException {
+ OzoneClient client = new OzoneClientStub();
+ client.getObjectStore().createS3Bucket(BUCKET_NAME);
+
+ HttpHeaders headers = mock(HttpHeaders.class);
+
when(headers.getHeaderString(X_AMZ_CONTENT_SHA256)).thenReturn("UNSIGNED-PAYLOAD");
+ when(headers.getHeaderString(STORAGE_CLASS_HEADER)).thenReturn("STANDARD");
+
+ rest = EndpointBuilder.newBucketEndpointBuilder()
+ .setClient(client)
+ .setHeaders(headers)
+ .build();
+ }
+
+ @Test
+ public void testPutBucketTaggingWithEmptyBody() {
+ assertErrorResponse(MALFORMED_XML, () -> putBucketTagging(rest,
BUCKET_NAME, ""));
+ }
+
+ @Test
+ public void testPutValidBucketTagging() throws Exception {
+ assertSucceeds(() -> putBucketTagging(rest, BUCKET_NAME, twoTagsBody()));
+ Map<String, String> tags =
+
rest.getClient().getObjectStore().getS3Bucket(BUCKET_NAME).getBucketTagging();
+ assertThat(tags).containsExactlyInAnyOrderEntriesOf(
+ ImmutableMap.of("tag1", "value1", "tag2", "value2"));
+ }
+
+ @Test
+ public void testPutInvalidBucketTaggingXml() {
+ assertErrorResponse(MALFORMED_XML, () -> putBucketTagging(rest,
BUCKET_NAME, null));
+ assertErrorResponse(MALFORMED_XML, () -> putBucketTagging(rest,
BUCKET_NAME, invalidXmlStructure()));
+ assertErrorResponse(MALFORMED_XML, () -> putBucketTagging(rest,
BUCKET_NAME, noTagSet()));
+ assertErrorResponse(MALFORMED_XML, () -> putBucketTagging(rest,
BUCKET_NAME, emptyTags()));
+ assertErrorResponse(MALFORMED_XML, () -> putBucketTagging(rest,
BUCKET_NAME, tagKeyNotSpecified()));
+ assertErrorResponse(MALFORMED_XML, () -> putBucketTagging(rest,
BUCKET_NAME, tagValueNotSpecified()));
+ }
+
+ @Test
+ public void testPutBucketTaggingDuplicateKeys() {
+ assertErrorResponse(INVALID_TAG, () -> putBucketTagging(rest, BUCKET_NAME,
duplicateTagKeys()));
+ }
+
+ @Test
+ public void testPutBucketTaggingAwsPrefixKey() {
+ assertErrorResponse(INVALID_TAG, () -> putBucketTagging(rest, BUCKET_NAME,
awsPrefixTag()));
+ }
+
+ @Test
+ public void testPutBucketTaggingKeyTooLong() {
+ String longKey = StringUtils.repeat("k", TAG_KEY_LENGTH_LIMIT + 1);
+ assertErrorResponse(INVALID_TAG, () -> putBucketTagging(rest, BUCKET_NAME,
singleTag(longKey, "v")));
+ }
+
+ @Test
+ public void testPutBucketTaggingValueTooLong() {
+ String longValue = StringUtils.repeat("v", TAG_VALUE_LENGTH_LIMIT + 1);
+ assertErrorResponse(INVALID_TAG, () -> putBucketTagging(rest, BUCKET_NAME,
singleTag("k", longValue)));
+ }
+
+ @Test
+ public void testPutBucketTaggingExceedsLimit() {
+ StringBuilder tags = new StringBuilder("<Tagging xmlns=\"" +
S3Consts.S3_XML_NAMESPACE + "\"><TagSet>");
+ for (int i = 0; i < TAG_BUCKET_NUM_LIMIT + 1; i++) {
+
tags.append("<Tag><Key>tag").append(i).append("</Key><Value>value").append(i)
+ .append("</Value></Tag>");
+ }
+ tags.append("</TagSet></Tagging>");
+ assertErrorResponse(INVALID_TAG, () -> putBucketTagging(rest, BUCKET_NAME,
tags.toString()));
+ }
+
+ @Test
+ public void testPutBucketTaggingNoBucketFound() {
+ assertErrorResponse(NO_SUCH_BUCKET, () -> putBucketTagging(rest,
"nonexistent", twoTagsBody()));
+ }
+
+ private static String twoTagsBody() {
+ return
+ "<Tagging xmlns=\"" + S3Consts.S3_XML_NAMESPACE + "\">" +
+ " <TagSet>" +
+ " <Tag>" +
+ " <Key>tag1</Key>" +
+ " <Value>value1</Value>" +
+ " </Tag>" +
+ " <Tag>" +
+ " <Key>tag2</Key>" +
+ " <Value>value2</Value>" +
+ " </Tag>" +
+ " </TagSet>" +
+ "</Tagging>";
+ }
+
+ private static String singleTag(String key, String value) {
+ return
+ "<Tagging xmlns=\"" + S3Consts.S3_XML_NAMESPACE + "\">" +
+ " <TagSet>" +
+ " <Tag>" +
+ " <Key>" + key + "</Key>" +
+ " <Value>" + value + "</Value>" +
+ " </Tag>" +
+ " </TagSet>" +
+ "</Tagging>";
+ }
+
+ private static String duplicateTagKeys() {
+ return
+ "<Tagging xmlns=\"" + S3Consts.S3_XML_NAMESPACE + "\">" +
+ " <TagSet>" +
+ " <Tag><Key>dup</Key><Value>v1</Value></Tag>" +
+ " <Tag><Key>dup</Key><Value>v2</Value></Tag>" +
+ " </TagSet>" +
+ "</Tagging>";
+ }
+
+ private static String awsPrefixTag() {
+ return singleTag("aws:reserved", "value");
+ }
+
+ private static String invalidXmlStructure() {
+ return
+ "<Tagging xmlns=\"" + S3Consts.S3_XML_NAMESPACE + "\">" +
+ " <TagSet>" +
+ " </Ta" +
+ "Tagging>";
+ }
+
+ private static String noTagSet() {
+ return "<Tagging xmlns=\"" + S3Consts.S3_XML_NAMESPACE + "\"></Tagging>";
+ }
+
+ private static String emptyTags() {
+ return
+ "<Tagging xmlns=\"" + S3Consts.S3_XML_NAMESPACE + "\">" +
+ " <TagSet></TagSet>" +
+ "</Tagging>";
+ }
+
+ private static String tagKeyNotSpecified() {
+ return
+ "<Tagging xmlns=\"" + S3Consts.S3_XML_NAMESPACE + "\">" +
+ " <TagSet><Tag><Value>val1</Value></Tag></TagSet>" +
+ "</Tagging>";
+ }
+
+ private static String tagValueNotSpecified() {
+ return
+ "<Tagging xmlns=\"" + S3Consts.S3_XML_NAMESPACE + "\">" +
+ " <TagSet><Tag><Key>tag1</Key></Tag></TagSet>" +
+ "</Tagging>";
+ }
+}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/metrics/TestS3GatewayMetrics.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/metrics/TestS3GatewayMetrics.java
index 5a4be9dd975..26c1e723128 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/metrics/TestS3GatewayMetrics.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/metrics/TestS3GatewayMetrics.java
@@ -26,11 +26,14 @@
import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertSucceeds;
import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.completeMultipartUpload;
import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.delete;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.deleteBucketTagging;
import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.deleteTagging;
import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.get;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.getBucketTagging;
import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.getTagging;
import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.listParts;
import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.put;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.putBucketTagging;
import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.putTagging;
import static
org.apache.hadoop.ozone.s3.exception.S3ErrorTable.BUCKET_ALREADY_EXISTS;
import static org.apache.hadoop.ozone.s3.util.S3Consts.COPY_SOURCE_HEADER;
@@ -566,6 +569,68 @@ public void testDeleteObjectTaggingFailure() {
assertEquals(1L, curMetric - oriMetric);
}
+ @Test
+ public void testPutBucketTaggingSuccess() throws Exception {
+ long oriMetric = metrics.getPutBucketTaggingSuccess();
+ assertSucceeds(() -> putBucketTagging(bucketEndpoint, bucketName,
getPutBucketTaggingBody()));
+
+ long curMetric = metrics.getPutBucketTaggingSuccess();
+ assertEquals(1L, curMetric - oriMetric);
+ }
+
+ @Test
+ public void testPutBucketTaggingFailure() {
+ long oriMetric = metrics.getPutBucketTaggingFailure();
+
+ assertErrorResponse(S3ErrorTable.NO_SUCH_BUCKET,
+ () -> putBucketTagging(bucketEndpoint, "nonexistent",
getPutBucketTaggingBody()));
+
+ long curMetric = metrics.getPutBucketTaggingFailure();
+ assertEquals(1L, curMetric - oriMetric);
+ }
+
+ @Test
+ public void testGetBucketTaggingSuccess() throws Exception {
+ long oriMetric = metrics.getGetBucketTaggingSuccess();
+ assertSucceeds(() -> putBucketTagging(bucketEndpoint, bucketName,
getPutBucketTaggingBody()));
+ assertSucceeds(() -> getBucketTagging(bucketEndpoint, bucketName));
+
+ long curMetric = metrics.getGetBucketTaggingSuccess();
+ assertEquals(1L, curMetric - oriMetric);
+ }
+
+ @Test
+ public void testGetBucketTaggingFailure() {
+ long oriMetric = metrics.getGetBucketTaggingFailure();
+
+ assertErrorResponse(S3ErrorTable.NO_SUCH_TAG_SET,
+ () -> getBucketTagging(bucketEndpoint, bucketName));
+
+ long curMetric = metrics.getGetBucketTaggingFailure();
+ assertEquals(1L, curMetric - oriMetric);
+ }
+
+ @Test
+ public void testDeleteBucketTaggingSuccess() throws Exception {
+ long oriMetric = metrics.getDeleteBucketTaggingSuccess();
+ assertSucceeds(() -> putBucketTagging(bucketEndpoint, bucketName,
getPutBucketTaggingBody()));
+ deleteBucketTagging(bucketEndpoint, bucketName);
+
+ long curMetric = metrics.getDeleteBucketTaggingSuccess();
+ assertEquals(1L, curMetric - oriMetric);
+ }
+
+ @Test
+ public void testDeleteBucketTaggingFailure() {
+ long oriMetric = metrics.getDeleteBucketTaggingFailure();
+
+ assertErrorResponse(S3ErrorTable.NO_SUCH_BUCKET,
+ () -> deleteBucketTagging(bucketEndpoint, "nonexistent"));
+
+ long curMetric = metrics.getDeleteBucketTaggingFailure();
+ assertEquals(1L, curMetric - oriMetric);
+ }
+
private String initiateMultipartUpload(String bktName, String key) throws
IOException, OS3Exception {
return EndpointTestUtils.initiateMultipartUpload(keyEndpoint, bktName,
key);
}
@@ -582,6 +647,10 @@ private static String getPutTaggingBody() {
"</Tagging>";
}
+ private static String getPutBucketTaggingBody() {
+ return getPutTaggingBody();
+ }
+
@Test
public void testPutObjectAclLatencyMetricsSnapshot() {
MetricsCollectorImpl collector = new MetricsCollectorImpl();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]