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]

Reply via email to