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 c86da017e90 HDDS-14907. Conditional Delete (DeleteObject) (#10511)
c86da017e90 is described below
commit c86da017e90a806d3cd373b889a3c0bca09ab531
Author: Peter Lee <[email protected]>
AuthorDate: Sat Jun 27 09:16:11 2026 +0800
HDDS-14907. Conditional Delete (DeleteObject) (#10511)
---
.../ozone/client/protocol/ClientProtocol.java | 14 +++
.../apache/hadoop/ozone/client/rpc/RpcClient.java | 17 ++-
...OzoneManagerProtocolClientSideTranslatorPB.java | 7 +-
.../ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java | 81 +++++++++++++
.../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java | 67 +++++++++++
.../ozone/om/request/key/OMKeyDeleteRequest.java | 2 +
.../om/request/key/OMKeyDeleteRequestWithFSO.java | 1 +
.../om/request/key/TestOMKeyDeleteRequest.java | 131 ++++++++++++++++++++-
.../hadoop/ozone/s3/endpoint/ObjectEndpoint.java | 14 ++-
.../ozone/s3/endpoint/S3ConditionalRequest.java | 27 +++++
.../hadoop/ozone/client/ClientProtocolStub.java | 8 ++
.../hadoop/ozone/client/OzoneBucketStub.java | 23 ++++
.../hadoop/ozone/s3/endpoint/TestObjectDelete.java | 76 ++++++++++++
13 files changed, 459 insertions(+), 9 deletions(-)
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index 8653048f8b2..ee5facd8edd 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -541,6 +541,20 @@ void deleteKey(String volumeName, String bucketName,
String keyName,
boolean recursive)
throws IOException;
+ /**
+ * Deletes an existing key if the key's current ETag matches expectedETag.
+ * @param volumeName Name of the Volume
+ * @param bucketName Name of the Bucket
+ * @param keyName Name of the Key
+ * @param recursive recursive deletion of all sub path keys if true,
+ * otherwise non-recursive
+ * @param expectedETag expected ETag, or "*" to require the key to exist
+ * @throws IOException
+ */
+ void deleteKey(String volumeName, String bucketName, String keyName,
+ boolean recursive, String expectedETag)
+ throws IOException;
+
/**
* Deletes keys through the list.
* @param volumeName Name of the Volume
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index f1611a117dd..9aec4702e28 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -1706,16 +1706,25 @@ public OzoneInputStream getKey(
public void deleteKey(
String volumeName, String bucketName, String keyName, boolean recursive)
throws IOException {
+ deleteKey(volumeName, bucketName, keyName, recursive, null);
+ }
+
+ @Override
+ public void deleteKey(
+ String volumeName, String bucketName, String keyName, boolean recursive,
+ String expectedETag) throws IOException {
verifyVolumeName(volumeName);
verifyBucketName(bucketName);
Objects.requireNonNull(keyName, "keyName == null");
- OmKeyArgs keyArgs = new OmKeyArgs.Builder()
+ OmKeyArgs.Builder keyArgs = new OmKeyArgs.Builder()
.setVolumeName(volumeName)
.setBucketName(bucketName)
.setKeyName(keyName)
- .setRecursive(recursive)
- .build();
- ozoneManagerClient.deleteKey(keyArgs);
+ .setRecursive(recursive);
+ if (expectedETag != null) {
+ keyArgs.setExpectedETag(expectedETag);
+ }
+ ozoneManagerClient.deleteKey(keyArgs.build());
}
@Override
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index 9ca5ff63f8b..405c3beacae 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -954,11 +954,14 @@ public void renameKey(OmKeyArgs args, String toKeyName)
throws IOException {
@Override
public void deleteKey(OmKeyArgs args) throws IOException {
DeleteKeyRequest.Builder req = DeleteKeyRequest.newBuilder();
- KeyArgs keyArgs = KeyArgs.newBuilder()
+ KeyArgs.Builder keyArgs = KeyArgs.newBuilder()
.setVolumeName(args.getVolumeName())
.setBucketName(args.getBucketName())
.setKeyName(args.getKeyName())
- .setRecursive(args.isRecursive()).build();
+ .setRecursive(args.isRecursive());
+ if (args.getExpectedETag() != null) {
+ keyArgs.setExpectedETag(args.getExpectedETag());
+ }
req.setKeyArgs(keyArgs);
OMRequest omRequest = createOMRequest(Type.DeleteKey)
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 0e5ed3e616e..5de874b51d0 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
@@ -500,6 +500,87 @@ public void testPutObjectIfMatchMissingKeyFail() {
assertEquals("NoSuchKey", missingKey.getErrorCode());
}
+ @Test
+ public void testDeleteObjectIfMatch() throws IOException {
+ final String bucketName = getBucketName();
+ final String keyName = getKeyName();
+ final String content = "bar";
+ s3Client.createBucket(bucketName);
+
+ InputStream is = new
ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+ PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName,
is, new ObjectMetadata());
+
+ int responseCode = deleteObjectWithIfMatch(bucketName, keyName,
putObjectResult.getETag());
+
+ assertEquals(HttpURLConnection.HTTP_NO_CONTENT, responseCode);
+ assertFalse(s3Client.doesObjectExist(bucketName, keyName));
+ }
+
+ @Test
+ public void testDeleteObjectIfMatchFail() throws IOException {
+ final String bucketName = getBucketName();
+ final String keyName = getKeyName();
+ final String content = "bar";
+ s3Client.createBucket(bucketName);
+
+ InputStream is = new
ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+ PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName,
is, new ObjectMetadata());
+
+ int responseCode = deleteObjectWithIfMatch(bucketName, keyName,
"wrong-etag");
+
+ assertEquals(HttpURLConnection.HTTP_PRECON_FAILED, responseCode);
+ ObjectMetadata existingObjectMetadata =
s3Client.getObjectMetadata(bucketName, keyName);
+ assertEquals(putObjectResult.getETag(), existingObjectMetadata.getETag());
+ }
+
+ @Test
+ public void testDeleteObjectIfMatchWildcard() throws IOException {
+ final String bucketName = getBucketName();
+ final String keyName = getKeyName();
+ final String content = "bar";
+ s3Client.createBucket(bucketName);
+
+ InputStream is = new
ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+ s3Client.putObject(bucketName, keyName, is, new ObjectMetadata());
+
+ int responseCode = deleteObjectWithIfMatch(bucketName, keyName, "*");
+
+ assertEquals(HttpURLConnection.HTTP_NO_CONTENT, responseCode);
+ assertFalse(s3Client.doesObjectExist(bucketName, keyName));
+ }
+
+ @Test
+ public void testDeleteObjectIfMatchWildcardMissingKeyFail() throws
IOException {
+ final String bucketName = getBucketName();
+ final String keyName = getKeyName();
+ s3Client.createBucket(bucketName);
+
+ int responseCode = deleteObjectWithIfMatch(bucketName, keyName, "*");
+
+ assertEquals(HttpURLConnection.HTTP_PRECON_FAILED, responseCode);
+ assertFalse(s3Client.doesObjectExist(bucketName, keyName));
+ }
+
+ private int deleteObjectWithIfMatch(String bucketName, String keyName,
String ifMatch) throws IOException {
+ GeneratePresignedUrlRequest request = new
GeneratePresignedUrlRequest(bucketName, keyName)
+ .withMethod(HttpMethod.DELETE)
+ .withExpiration(Date.from(Instant.now().plusMillis(1000 * 60 * 60)));
+ request.putCustomRequestHeader(Headers.GET_OBJECT_IF_MATCH, ifMatch);
+ URL presignedUrl = s3Client.generatePresignedUrl(request);
+ Map<String, List<String>> headers =
Collections.singletonMap(Headers.GET_OBJECT_IF_MATCH,
+ Collections.singletonList(ifMatch));
+
+ HttpURLConnection connection = null;
+ try {
+ connection = S3SDKTestUtils.openHttpURLConnection(presignedUrl,
"DELETE", headers, null);
+ return connection.getResponseCode();
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
@Test
public void testCopyObject() {
final String sourceBucketName = getBucketName("source");
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 753e3826949..ff46b739d9f 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
@@ -406,6 +406,73 @@ public void testPutObjectIfMatchMissingKeyFail() {
b -> b.bucket(bucketName).key(keyName)));
}
+ @Test
+ public void testDeleteObjectIfMatch() {
+ final String bucketName = getBucketName();
+ final String keyName = getKeyName();
+ final String content = "bar";
+ s3Client.createBucket(b -> b.bucket(bucketName));
+
+ PutObjectResponse initialResponse = s3Client.putObject(
+ b -> b.bucket(bucketName).key(keyName),
RequestBody.fromString(content));
+
+ s3Client.deleteObject(b ->
b.bucket(bucketName).key(keyName).ifMatch(initialResponse.eTag()));
+
+ assertThrows(NoSuchKeyException.class, () -> s3Client.headObject(
+ b -> b.bucket(bucketName).key(keyName)));
+ }
+
+ @Test
+ public void testDeleteObjectIfMatchFail() {
+ final String bucketName = getBucketName();
+ final String keyName = getKeyName();
+ final String content = "bar";
+ s3Client.createBucket(b -> b.bucket(bucketName));
+
+ PutObjectResponse initialResponse = s3Client.putObject(
+ b -> b.bucket(bucketName).key(keyName),
RequestBody.fromString(content));
+
+ S3Exception exception = assertThrows(S3Exception.class,
+ () -> s3Client.deleteObject(b ->
b.bucket(bucketName).key(keyName).ifMatch("wrong-etag")));
+
+ assertEquals(412, exception.statusCode());
+ assertEquals("PreconditionFailed",
exception.awsErrorDetails().errorCode());
+
+ HeadObjectResponse headObjectResponse = s3Client.headObject(
+ b -> b.bucket(bucketName).key(keyName));
+ assertEquals(initialResponse.eTag(), headObjectResponse.eTag());
+ }
+
+ @Test
+ public void testDeleteObjectIfMatchWildcard() {
+ final String bucketName = getBucketName();
+ final String keyName = getKeyName();
+ final String content = "bar";
+ s3Client.createBucket(b -> b.bucket(bucketName));
+
+ s3Client.putObject(b -> b.bucket(bucketName).key(keyName),
RequestBody.fromString(content));
+
+ s3Client.deleteObject(b -> b.bucket(bucketName).key(keyName).ifMatch("*"));
+
+ assertThrows(NoSuchKeyException.class, () -> s3Client.headObject(
+ b -> b.bucket(bucketName).key(keyName)));
+ }
+
+ @Test
+ public void testDeleteObjectIfMatchWildcardMissingKeyFail() {
+ final String bucketName = getBucketName();
+ final String keyName = getKeyName();
+ s3Client.createBucket(b -> b.bucket(bucketName));
+
+ S3Exception exception = assertThrows(S3Exception.class,
+ () -> s3Client.deleteObject(b ->
b.bucket(bucketName).key(keyName).ifMatch("*")));
+
+ assertEquals(412, exception.statusCode());
+ assertEquals("PreconditionFailed",
exception.awsErrorDetails().errorCode());
+ assertThrows(NoSuchKeyException.class, () -> s3Client.headObject(
+ b -> b.bucket(bucketName).key(keyName)));
+ }
+
@Test
public void testGetObjectIfMatch() {
final String bucketName = getBucketName();
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequest.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequest.java
index 43b0e2a9f8f..820c4d17197 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequest.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequest.java
@@ -146,6 +146,8 @@ public OMClientResponse validateAndUpdateCache(OzoneManager
ozoneManager, Execut
throw new OMException("Key not found", KEY_NOT_FOUND);
}
+ validateIfMatchETag(keyArgs, omKeyInfo);
+
// Set the UpdateID to current transactionLogIndex
omKeyInfo = omKeyInfo.toBuilder()
.setUpdateID(trxnLogIndex)
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequestWithFSO.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequestWithFSO.java
index efe04a804b9..769b2e43a5b 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequestWithFSO.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequestWithFSO.java
@@ -118,6 +118,7 @@ public OMClientResponse validateAndUpdateCache(OzoneManager
ozoneManager, Execut
}
OmKeyInfo omKeyInfo = keyStatus.getKeyInfo();
+ validateIfMatchETag(keyArgs, omKeyInfo);
// New key format for the fileTable & dirTable.
// For example, the user given key path is '/a/b/c/d/e/file1', then in DB
// keyName field stores only the leaf node name, which is 'file1'.
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyDeleteRequest.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyDeleteRequest.java
index 08d87cdd8fc..8aba4c88683 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyDeleteRequest.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyDeleteRequest.java
@@ -24,6 +24,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.UUID;
+import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
@@ -145,6 +146,109 @@ public void
testValidateAndUpdateCacheWithBucketNotFound() throws Exception {
omClientResponse.getOMResponse().getStatus());
}
+ @Test
+ public void testValidateAndUpdateCacheWithExpectedETagSuccess()
+ throws Exception {
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, getBucketLayout());
+
+ String ozoneKey = addKeyToTableWithETag("matching-etag");
+
+ OMRequest modifiedOmRequest =
+ doPreExecute(createDeleteKeyRequestWithExpectedETag("matching-etag"));
+ OMKeyDeleteRequest omKeyDeleteRequest =
+ getOmKeyDeleteRequest(modifiedOmRequest);
+
+ OMClientResponse omClientResponse =
+ omKeyDeleteRequest.validateAndUpdateCache(ozoneManager, 100L);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ omClientResponse.getOMResponse().getStatus());
+ assertNull(omMetadataManager.getKeyTable(getBucketLayout()).get(ozoneKey));
+ }
+
+ @Test
+ public void testValidateAndUpdateCacheWithExpectedETagMismatch()
+ throws Exception {
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, getBucketLayout());
+
+ String ozoneKey = addKeyToTableWithETag("actual-etag");
+
+ OMRequest modifiedOmRequest =
+ doPreExecute(createDeleteKeyRequestWithExpectedETag("expected-etag"));
+ OMKeyDeleteRequest omKeyDeleteRequest =
+ getOmKeyDeleteRequest(modifiedOmRequest);
+
+ OMClientResponse omClientResponse =
+ omKeyDeleteRequest.validateAndUpdateCache(ozoneManager, 100L);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.ETAG_MISMATCH,
+ omClientResponse.getOMResponse().getStatus());
+ assertNotNull(omMetadataManager.getKeyTable(getBucketLayout())
+ .get(ozoneKey));
+ }
+
+ @Test
+ public void testValidateAndUpdateCacheWithExpectedETagMissingOnKey()
+ throws Exception {
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, getBucketLayout());
+
+ String ozoneKey = addKeyToTable();
+
+ OMRequest modifiedOmRequest =
+ doPreExecute(createDeleteKeyRequestWithExpectedETag("expected-etag"));
+ OMKeyDeleteRequest omKeyDeleteRequest =
+ getOmKeyDeleteRequest(modifiedOmRequest);
+
+ OMClientResponse omClientResponse =
+ omKeyDeleteRequest.validateAndUpdateCache(ozoneManager, 100L);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.ETAG_NOT_AVAILABLE,
+ omClientResponse.getOMResponse().getStatus());
+ assertNotNull(omMetadataManager.getKeyTable(getBucketLayout())
+ .get(ozoneKey));
+ }
+
+ @Test
+ public void testValidateAndUpdateCacheWithExpectedWildcardETag()
+ throws Exception {
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, getBucketLayout());
+
+ String ozoneKey = addKeyToTable();
+
+ OMRequest modifiedOmRequest =
+ doPreExecute(createDeleteKeyRequestWithExpectedETag("*"));
+ OMKeyDeleteRequest omKeyDeleteRequest =
+ getOmKeyDeleteRequest(modifiedOmRequest);
+
+ OMClientResponse omClientResponse =
+ omKeyDeleteRequest.validateAndUpdateCache(ozoneManager, 100L);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.ETAG_NOT_AVAILABLE,
+ omClientResponse.getOMResponse().getStatus());
+ assertNotNull(omMetadataManager.getKeyTable(getBucketLayout())
+ .get(ozoneKey));
+ }
+
+ @Test
+ public void testValidateAndUpdateCacheWithExpectedWildcardETagKeyNotFound()
+ throws Exception {
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, getBucketLayout());
+
+ OMKeyDeleteRequest omKeyDeleteRequest =
+ getOmKeyDeleteRequest(createDeleteKeyRequest("missing-key", "*"));
+
+ OMClientResponse omClientResponse =
+ omKeyDeleteRequest.validateAndUpdateCache(ozoneManager, 100L);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.KEY_NOT_FOUND,
+ omClientResponse.getOMResponse().getStatus());
+ }
+
/**
* This method calls preExecute and verify the modified request.
* @param originalOmRequest
@@ -173,8 +277,21 @@ protected OMRequest createDeleteKeyRequest() {
}
protected OMRequest createDeleteKeyRequest(String testKeyName) {
- KeyArgs keyArgs = KeyArgs.newBuilder().setBucketName(bucketName)
- .setVolumeName(volumeName).setKeyName(testKeyName).build();
+ return createDeleteKeyRequest(testKeyName, null);
+ }
+
+ protected OMRequest createDeleteKeyRequestWithExpectedETag(
+ String expectedETag) {
+ return createDeleteKeyRequest(keyName, expectedETag);
+ }
+
+ protected OMRequest createDeleteKeyRequest(
+ String testKeyName, String expectedETag) {
+ KeyArgs.Builder keyArgs = KeyArgs.newBuilder().setBucketName(bucketName)
+ .setVolumeName(volumeName).setKeyName(testKeyName);
+ if (expectedETag != null) {
+ keyArgs.setExpectedETag(expectedETag);
+ }
DeleteKeyRequest deleteKeyRequest =
DeleteKeyRequest.newBuilder().setKeyArgs(keyArgs).build();
@@ -196,6 +313,16 @@ protected String addKeyToTable(String key) throws
Exception {
return omMetadataManager.getOzoneKey(volumeName, bucketName, key);
}
+ protected String addKeyToTableWithETag(String eTag) throws Exception {
+ String ozoneKey = addKeyToTable();
+ OmKeyInfo omKeyInfo = omMetadataManager.getKeyTable(getBucketLayout())
+ .get(ozoneKey);
+ omMetadataManager.getKeyTable(getBucketLayout()).put(ozoneKey,
+ omKeyInfo.withMetadataMutations(
+ metadata -> metadata.put(OzoneConsts.ETAG, eTag)));
+ return ozoneKey;
+ }
+
protected OMKeyDeleteRequest getOmKeyDeleteRequest(
OMRequest modifiedOmRequest) {
return new OMKeyDeleteRequest(modifiedOmRequest, BucketLayout.DEFAULT);
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
index 72f0c672bd3..4e3b449bf0d 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
@@ -654,20 +654,32 @@ Response handleDeleteRequest(ObjectRequestContext
context, String keyPath)
throws IOException, OS3Exception {
final long startNanos = context.getStartNanos();
+ S3ConditionalRequest.DeleteCondition deleteCondition = null;
try {
OzoneVolume volume = context.getVolume();
+ deleteCondition =
S3ConditionalRequest.parseDeleteCondition(getHeaders(), keyPath);
- getClientProtocol().deleteKey(volume.getName(), context.getBucketName(),
keyPath, false);
+ if (!deleteCondition.hasIfMatch()) {
+ getClientProtocol().deleteKey(volume.getName(),
context.getBucketName(), keyPath, false);
+ } else {
+ getClientProtocol().deleteKey(volume.getName(),
context.getBucketName(), keyPath, false,
+ deleteCondition.getExpectedETag());
+ }
getMetrics().updateDeleteKeySuccessStats(startNanos);
return Response.status(Status.NO_CONTENT).build();
} catch (OMException ex) {
getMetrics().updateDeleteKeyFailureStats(startNanos);
if (ex.getResult() == ResultCodes.KEY_NOT_FOUND) {
+ if (deleteCondition != null && deleteCondition.hasIfMatch()) {
+ throw newError(PRECOND_FAILED, keyPath, ex);
+ }
//NOT_FOUND is not a problem, AWS doesn't throw exception for missing
// keys. Just return 204
return Response.status(Status.NO_CONTENT).build();
+ } else if (ex.getResult() == ResultCodes.ETAG_MISMATCH || ex.getResult()
== ResultCodes.ETAG_NOT_AVAILABLE) {
+ throw newError(PRECOND_FAILED, keyPath, ex);
} else if (ex.getResult() == ResultCodes.DIRECTORY_NOT_EMPTY) {
// With PREFIX metadata layout, a dir deletion without recursive flag
// to true will throw DIRECTORY_NOT_EMPTY error for a non-empty dir.
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3ConditionalRequest.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3ConditionalRequest.java
index 259abd7d4ed..e7826c294bc 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3ConditionalRequest.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3ConditionalRequest.java
@@ -189,6 +189,17 @@ static WriteConditions parseWriteConditions(HttpHeaders
headers,
return new WriteConditions(trimmedIfNoneMatch, trimmedIfMatch);
}
+ static DeleteCondition parseDeleteCondition(HttpHeaders headers, String
keyPath)
+ throws OS3Exception {
+ String ifMatch = headers.getHeaderString(S3Consts.IF_MATCH_HEADER);
+ if (ifMatch != null && StringUtils.isBlank(ifMatch)) {
+ OS3Exception ex = newError(INVALID_REQUEST, keyPath);
+ ex.setErrorMessage("If-Match header cannot be empty.");
+ throw ex;
+ }
+ return new DeleteCondition(ifMatch);
+ }
+
private static Response buildNotModifiedResponse(OzoneKey key) {
ResponseBuilder responseBuilder = Response.status(Status.NOT_MODIFIED);
ObjectEndpoint.addEntityTagHeader(responseBuilder, key);
@@ -288,4 +299,20 @@ String getExpectedETag() {
return parseETag(ifMatch);
}
}
+
+ static final class DeleteCondition {
+ private final String ifMatch;
+
+ private DeleteCondition(String ifMatch) {
+ this.ifMatch = ifMatch;
+ }
+
+ boolean hasIfMatch() {
+ return ifMatch != null;
+ }
+
+ String getExpectedETag() {
+ return parseETag(ifMatch);
+ }
+ }
}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
index 0ef0a790388..17c65ce7862 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
@@ -313,6 +313,14 @@ public void deleteKey(String volumeName, String
bucketName, String keyName,
getBucket(volumeName, bucketName).deleteKey(keyName);
}
+ @Override
+ public void deleteKey(String volumeName, String bucketName, String keyName,
+ boolean recursive, String expectedETag)
+ throws IOException {
+ ((OzoneBucketStub) getBucket(volumeName, bucketName))
+ .deleteKey(keyName, expectedETag);
+ }
+
@Override
public void deleteKeys(String volumeName, String bucketName,
List<String> keyNameList) throws IOException {
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java
index 10ed365b2b0..2b6c5ab6a08 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java
@@ -479,6 +479,29 @@ public void deleteKey(String key) throws IOException {
keyDetails.remove(key);
}
+ public void deleteKey(String key, String expectedETag) throws IOException {
+ if (expectedETag == null) {
+ deleteKey(key);
+ return;
+ }
+ OzoneKeyDetails existing = keyDetails.get(key);
+ if (existing == null) {
+ throw new OMException("Key not found for If-Match",
+ ResultCodes.KEY_NOT_FOUND);
+ }
+ if (!"*".equals(expectedETag)) {
+ if (!existing.hasEtag()) {
+ throw new OMException("Key does not have an ETag",
+ ResultCodes.ETAG_NOT_AVAILABLE);
+ }
+ if (!existing.isEtagEquals(expectedETag)) {
+ throw new OMException("ETag mismatch",
+ ResultCodes.ETAG_MISMATCH);
+ }
+ }
+ deleteKey(key);
+ }
+
@Override
public Map<String, ErrorInfo> deleteKeys(List<String> keyList, boolean
quiet) throws IOException {
Map<String, ErrorInfo> keyErrorMap = new HashMap<>();
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectDelete.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectDelete.java
index 595365972ec..00114e3977c 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectDelete.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectDelete.java
@@ -17,11 +17,21 @@
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.assertStatus;
import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.delete;
+import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.put;
+import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.PRECOND_FAILED;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.IF_MATCH_HEADER;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.X_AMZ_CONTENT_SHA256;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import java.io.IOException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
import org.apache.hadoop.ozone.client.OzoneBucket;
import org.apache.hadoop.ozone.client.OzoneClient;
import org.apache.hadoop.ozone.client.OzoneClientStub;
@@ -56,4 +66,70 @@ void testDelete() throws IOException, OS3Exception {
assertFalse(bucket.listKeys("").hasNext(),
"Bucket Should not contain any key after delete");
}
+
+ @Test
+ void testDeleteIfMatchWithMatchingETag() throws IOException, OS3Exception {
+ OzoneClient client = new OzoneClientStub();
+ client.getObjectStore().createS3Bucket("b1");
+
+ OzoneBucket bucket =
+ client.getObjectStore().getS3Bucket("b1");
+ HttpHeaders headers = mock(HttpHeaders.class);
+ when(headers.getHeaderString(X_AMZ_CONTENT_SHA256))
+ .thenReturn("UNSIGNED-PAYLOAD");
+
+ ObjectEndpoint rest = EndpointBuilder.newObjectEndpointBuilder()
+ .setClient(client)
+ .setHeaders(headers)
+ .build();
+
+ Response response = put(rest, "b1", "key1", "content");
+ String eTag = response.getHeaderString(HttpHeaders.ETAG);
+ assertNotNull(eTag);
+
+ when(headers.getHeaderString(IF_MATCH_HEADER)).thenReturn(eTag);
+ assertStatus(HttpStatus.SC_NO_CONTENT, () -> delete(rest, "b1", "key1"));
+
+ assertFalse(bucket.listKeys("").hasNext(),
+ "Bucket Should not contain any key after delete");
+ }
+
+ @Test
+ void testDeleteIfMatchWithMismatchingETag() throws IOException, OS3Exception
{
+ OzoneClient client = new OzoneClientStub();
+ client.getObjectStore().createS3Bucket("b1");
+
+ OzoneBucket bucket =
+ client.getObjectStore().getS3Bucket("b1");
+ HttpHeaders headers = mock(HttpHeaders.class);
+ when(headers.getHeaderString(X_AMZ_CONTENT_SHA256))
+ .thenReturn("UNSIGNED-PAYLOAD");
+
+ ObjectEndpoint rest = EndpointBuilder.newObjectEndpointBuilder()
+ .setClient(client)
+ .setHeaders(headers)
+ .build();
+
+ assertStatus(HttpStatus.SC_OK, () -> put(rest, "b1", "key1", "content"));
+
+
when(headers.getHeaderString(IF_MATCH_HEADER)).thenReturn("\"wrong-etag\"");
+ assertErrorResponse(PRECOND_FAILED, () -> delete(rest, "b1", "key1"));
+
+ assertNotNull(bucket.getKey("key1"));
+ }
+
+ @Test
+ void testDeleteIfMatchWildcardForMissingKey() throws IOException {
+ OzoneClient client = new OzoneClientStub();
+ client.getObjectStore().createS3Bucket("b1");
+ HttpHeaders headers = mock(HttpHeaders.class);
+ when(headers.getHeaderString(IF_MATCH_HEADER)).thenReturn("*");
+
+ ObjectEndpoint rest = EndpointBuilder.newObjectEndpointBuilder()
+ .setClient(client)
+ .setHeaders(headers)
+ .build();
+
+ assertErrorResponse(PRECOND_FAILED, () -> delete(rest, "b1", "missing"));
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]