This is an automated email from the ASF dual-hosted git repository.

sshenoy 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 30979b844c HDDS-8909. Support recursive bucket delete using ozone sh 
command. (#4981)
30979b844c is described below

commit 30979b844c7f5efac1e2a860668043d28e810f6e
Author: ashishkumar50 <[email protected]>
AuthorDate: Fri Jul 14 11:42:03 2023 +0530

    HDDS-8909. Support recursive bucket delete using ozone sh command. (#4981)
---
 .../hadoop/ozone/shell/TestOzoneShellHA.java       | 113 ++++++++++++++++++
 .../apache/hadoop/ozone/shell/OzoneAddress.java    |   4 +
 .../ozone/shell/bucket/DeleteBucketHandler.java    | 128 ++++++++++++++++++++-
 3 files changed, 243 insertions(+), 2 deletions(-)

diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/shell/TestOzoneShellHA.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/shell/TestOzoneShellHA.java
index fa7e9b0275..03d04feba0 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/shell/TestOzoneShellHA.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/shell/TestOzoneShellHA.java
@@ -1293,6 +1293,119 @@ public class TestOzoneShellHA {
     Assert.assertEquals(99, getNumOfKeys());
   }
 
+  @Test
+  public void testRecursiveBucketDelete()
+      throws Exception {
+    String volume1 = "volume50";
+    String bucket1 = "bucketfso";
+    String bucket2 = "bucketobs";
+    String bucket3 = "bucketlegacy";
+
+    // Create volume volume1
+    // Create bucket bucket1 with layout FILE_SYSTEM_OPTIMIZED
+    // Insert some keys into it
+    generateKeys(OZONE_URI_DELIMITER + volume1,
+        OZONE_URI_DELIMITER + bucket1,
+        BucketLayout.FILE_SYSTEM_OPTIMIZED.toString());
+
+    // Create OBS bucket in volume1
+    String[] args = new String[] {"bucket", "create", "--layout",
+        BucketLayout.OBJECT_STORE.toString(), volume1 +
+          OZONE_URI_DELIMITER + bucket2};
+    execute(ozoneShell, args);
+    out.reset();
+
+    // Insert few keys into OBS bucket
+    String keyName = OZONE_URI_DELIMITER + volume1 +
+        OZONE_URI_DELIMITER + bucket2 +
+        OZONE_URI_DELIMITER + "key";
+    for (int i = 0; i < 5; i++) {
+      args = new String[] {
+          "key", "put", "o3://" + omServiceId + keyName + i,
+          testFile.getPath()};
+      execute(ozoneShell, args);
+    }
+    out.reset();
+
+    // Create Legacy bucket in volume1
+    args = new String[] {"bucket", "create", "--layout",
+        BucketLayout.LEGACY.toString(), volume1 +
+          OZONE_URI_DELIMITER + bucket3};
+    execute(ozoneShell, args);
+    out.reset();
+
+    // Insert few keys into legacy bucket
+    keyName = OZONE_URI_DELIMITER + volume1 + OZONE_URI_DELIMITER + bucket3 +
+        OZONE_URI_DELIMITER + "key";
+    for (int i = 0; i < 5; i++) {
+      args = new String[] {
+          "key", "put", "o3://" + omServiceId + keyName + i,
+          testFile.getPath()};
+      execute(ozoneShell, args);
+    }
+    out.reset();
+
+    // Try bucket delete without recursive
+    // It should fail as bucket is not empty
+    final String[] args1 = new String[] {"bucket", "delete",
+        volume1 + OZONE_URI_DELIMITER + bucket1};
+    LambdaTestUtils.intercept(ExecutionException.class,
+        "BUCKET_NOT_EMPTY", () -> execute(ozoneShell, args1));
+    out.reset();
+
+    // bucket1 should still exist
+    Assert.assertEquals(client.getObjectStore()
+        .getVolume(volume1).getBucket(bucket1)
+        .getName(), bucket1);
+
+    // Delete bucket1 recursively
+    args =
+        new String[] {"bucket", "delete", volume1 +
+              OZONE_URI_DELIMITER + bucket1, "-r", "--yes"};
+    execute(ozoneShell, args);
+    out.reset();
+
+    // Bucket1 should not exist
+    LambdaTestUtils.intercept(OMException.class,
+        "BUCKET_NOT_FOUND", () -> client.getObjectStore()
+            .getVolume(volume1).getBucket(bucket1));
+
+    // Bucket2 and Bucket3 should still exist
+    Assert.assertEquals(client.getObjectStore().getVolume(volume1)
+        .getBucket(bucket2).getName(), bucket2);
+    Assert.assertEquals(client.getObjectStore().getVolume(volume1)
+        .getBucket(bucket3).getName(), bucket3);
+
+    // Delete bucket2(obs) recursively.
+    args =
+        new String[] {"bucket", "delete", volume1 +
+              OZONE_URI_DELIMITER + bucket2, "-r", "--yes"};
+    execute(ozoneShell, args);
+    out.reset();
+
+    LambdaTestUtils.intercept(OMException.class,
+        "BUCKET_NOT_FOUND", () -> client.getObjectStore()
+            .getVolume(volume1).getBucket(bucket2));
+
+    // Delete bucket3(legacy) recursively.
+    args =
+        new String[] {"bucket", "delete", volume1 +
+              OZONE_URI_DELIMITER + bucket3, "-r", "--yes"};
+    execute(ozoneShell, args);
+    out.reset();
+
+    LambdaTestUtils.intercept(OMException.class,
+        "BUCKET_NOT_FOUND", () -> client.getObjectStore()
+            .getVolume(volume1).getBucket(bucket3));
+
+    // Now delete volume without recursive
+    // All buckets are already deleted
+    args = new String[] {"volume", "delete", volume1};
+    execute(ozoneShell, args);
+    out.reset();
+    LambdaTestUtils.intercept(OMException.class,
+        "VOLUME_NOT_FOUND", () -> client.getObjectStore().getVolume(volume1));
+  }
 
   private void getVolume(String volumeName) {
     String[] args = new String[] {"volume", "create",
diff --git 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/OzoneAddress.java
 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/OzoneAddress.java
index cef95dcf0f..e9afce86a4 100644
--- 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/OzoneAddress.java
+++ 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/OzoneAddress.java
@@ -297,6 +297,10 @@ public class OzoneAddress {
     return bucketName;
   }
 
+  public String getOmHost() {
+    return ozoneURI.getHost();
+  }
+
   public String getSnapshotNameWithIndicator() {
     return snapshotNameWithIndicator;
   }
diff --git 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/bucket/DeleteBucketHandler.java
 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/bucket/DeleteBucketHandler.java
index a8eeee93e1..5626df2082 100644
--- 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/bucket/DeleteBucketHandler.java
+++ 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/bucket/DeleteBucketHandler.java
@@ -18,30 +18,154 @@
 
 package org.apache.hadoop.ozone.shell.bucket;
 
+import com.google.common.base.Strings;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.client.OzoneBucket;
 import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneKey;
 import org.apache.hadoop.ozone.client.OzoneVolume;
 import org.apache.hadoop.ozone.shell.OzoneAddress;
 
+import picocli.CommandLine;
 import picocli.CommandLine.Command;
 
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Scanner;
+
+import static org.apache.hadoop.fs.FileSystem.FS_DEFAULT_NAME_KEY;
+import static org.apache.hadoop.hdds.scm.net.NetConstants.PATH_SEPARATOR_STR;
+import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OFS_URI_SCHEME;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_ADDRESS_KEY;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_SERVICE_IDS_KEY;
+import static org.apache.hadoop.ozone.om.helpers.BucketLayout.OBJECT_STORE;
 
 /**
  * Delete bucket Handler.
  */
 @Command(name = "delete",
-    description = "deletes an empty bucket")
+    description = "deletes a bucket")
 public class DeleteBucketHandler extends BucketHandler {
 
+  @CommandLine.Option(
+      names = {"-r"},
+      description = "Delete bucket recursively"
+  )
+  private boolean bRecursive;
+  @CommandLine.Option(names = {"-y", "--yes"},
+      description = "Continue without interactive user confirmation")
+  private boolean yes;
+  private String omServiceId;
+  private static final int MAX_KEY_DELETE_BATCH_SIZE = 1000;
+
   @Override
   protected void execute(OzoneClient client, OzoneAddress address)
       throws IOException {
 
     String volumeName = address.getVolumeName();
     String bucketName = address.getBucketName();
-
     OzoneVolume vol = client.getObjectStore().getVolume(volumeName);
+
+    if (bRecursive) {
+      if (!Strings.isNullOrEmpty(address.getOmHost())) {
+        omServiceId = address.getOmHost();
+      } else {
+        Collection<String> serviceIds = getConf().getTrimmedStringCollection(
+            OZONE_OM_SERVICE_IDS_KEY);
+        if (serviceIds.size() == 1) {
+          // Only one OM service ID configured, we can use that
+          // If more than 1, it will fail in createClient step itself
+          omServiceId = serviceIds.iterator().next();
+        } else {
+          omServiceId = getConf().get(OZONE_OM_ADDRESS_KEY);
+        }
+      }
+      if (!yes) {
+        // Ask for user confirmation
+        out().print("This command will delete bucket recursively." +
+            "\nThere is no recovery option after using this command, " +
+            "and deleted keys won't move to trash." +
+            "\nEnter 'yes' to proceed': ");
+        out().flush();
+        Scanner scanner = new Scanner(new InputStreamReader(
+            System.in, StandardCharsets.UTF_8));
+        String confirmation = scanner.next().trim().toLowerCase();
+        if (!confirmation.equals("yes")) {
+          out().println("Operation cancelled.");
+          return;
+        }
+      }
+      OzoneBucket bucket = vol.getBucket(bucketName);
+      if (bucket.getBucketLayout().equals(OBJECT_STORE)) {
+        deleteOBSBucketRecursive(vol, bucket);
+      } else {
+        deleteFSBucketRecursive(vol, bucket);
+      }
+      return;
+    }
+    // Delete bucket without recursive
     vol.deleteBucket(bucketName);
+    out().printf("Bucket %s is deleted%n", bucketName);
   }
 
+  /**
+   * Delete OBS bucket recursively.
+   *
+   * @param bucket OzoneBucket
+   */
+  private void deleteOBSBucketRecursive(OzoneVolume vol, OzoneBucket bucket) {
+    ArrayList<String> keys = new ArrayList<>();
+    try {
+      if (!bucket.isLink()) {
+        Iterator<? extends OzoneKey> iterator = bucket.listKeys(null);
+        while (iterator.hasNext()) {
+          keys.add(iterator.next().getName());
+          if (MAX_KEY_DELETE_BATCH_SIZE == keys.size()) {
+            bucket.deleteKeys(keys);
+            keys.clear();
+          }
+        }
+        // delete if any remaining keys left
+        if (keys.size() > 0) {
+          bucket.deleteKeys(keys);
+        }
+      }
+      vol.deleteBucket(bucket.getName());
+      out().printf("Bucket %s is deleted%n", bucket.getName());
+    } catch (Exception e) {
+      out().printf("Could not delete bucket %s.%n", e.getMessage());
+    }
+  }
+
+  /**
+   * Delete Legacy/FSO bucket recursively.
+   *
+   * @param vol String
+   * @param bucket OzoneBucket
+   */
+  private void deleteFSBucketRecursive(OzoneVolume vol, OzoneBucket bucket) {
+    try {
+      String hostPrefix = OZONE_OFS_URI_SCHEME + "://" +
+          omServiceId + PATH_SEPARATOR_STR;
+      String ofsPrefix = hostPrefix + vol.getName() + PATH_SEPARATOR_STR +
+          bucket.getName();
+      final Path path = new Path(ofsPrefix);
+      OzoneConfiguration clientConf = new OzoneConfiguration(getConf());
+      clientConf.set(FS_DEFAULT_NAME_KEY, hostPrefix);
+      FileSystem fs = FileSystem.get(clientConf);
+      if (!fs.delete(path, true)) {
+        out().printf("Could not delete bucket %s.%n", bucket.getName());
+        return;
+      }
+      out().printf("Bucket %s is deleted%n", bucket.getName());
+    } catch (IOException e) {
+      out().printf("Could not delete bucket %s.%n", e.getMessage());
+    }
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to