Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: update BucketCleaner to clean up folders and managed folders in addition to objects #2693

Closed
wants to merge 11 commits into from
Closed
32 changes: 16 additions & 16 deletions .kokoro/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ mvn -version
echo ${JOB_TYPE}

# attempt to install 3 times with exponential backoff (starting with 10 seconds)
retry_with_backoff 3 10 \
#retry_with_backoff 3 10 \
mvn install -B -V -ntp \
-DskipTests=true \
-Dclirr.skip=true \
Expand All @@ -48,8 +48,8 @@ set +e
case ${JOB_TYPE} in
test)
echo "SUREFIRE_JVM_OPT: ${SUREFIRE_JVM_OPT}"
mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT}
RETURN_CODE=$?
# mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT}
RETURN_CODE=1
;;
lint)
mvn com.coveo:fmt-maven-plugin:check -B -ntp
Expand All @@ -60,25 +60,25 @@ javadoc)
RETURN_CODE=$?
;;
integration)
mvn -B ${INTEGRATION_TEST_ARGS} \
-ntp \
-Penable-integration-tests \
-DtrimStackTrace=false \
-Dclirr.skip=true \
-Denforcer.skip=true \
-fae \
verify
RETURN_CODE=$?
# mvn -B ${INTEGRATION_TEST_ARGS} \
# -ntp \
# -Penable-integration-tests \
# -DtrimStackTrace=false \
# -Dclirr.skip=true \
# -Denforcer.skip=true \
# -fae \
# verify
RETURN_CODE=1
;;
graalvm)
# Run Unit and Integration Tests with Native Image
mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test
RETURN_CODE=$?
# mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test
RETURN_CODE=1
;;
graalvm17)
# Run Unit and Integration Tests with Native Image
mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test
RETURN_CODE=$?
# mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test
RETURN_CODE=1
;;
samples)
SAMPLES_DIR=samples
Expand Down
5 changes: 0 additions & 5 deletions google-cloud-storage-control/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,6 @@
<artifactId>google-api-services-storage</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-storage</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
Expand Down

This file was deleted.

11 changes: 11 additions & 0 deletions google-cloud-storage/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,17 @@
<scope>test</scope>
<version>${pubsub-proto.version}</version>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-storage-control</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.api.grpc</groupId>
<artifactId>proto-google-cloud-storage-control-v2</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,42 @@

package com.google.cloud.storage.it;

import com.google.api.gax.grpc.GrpcCallContext;
import com.google.api.gax.paging.Page;
import com.google.api.gax.rpc.ApiException;
import com.google.api.gax.rpc.FailedPreconditionException;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.Storage.BlobField;
import com.google.cloud.storage.Storage.BlobListOption;
import com.google.cloud.storage.Storage.BlobSourceOption;
import com.google.cloud.storage.Storage.BucketSourceOption;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.storage.control.v2.BucketName;
import com.google.storage.control.v2.DeleteFolderRequest;
import com.google.storage.control.v2.DeleteManagedFolderRequest;
import com.google.storage.control.v2.Folder;
import com.google.storage.control.v2.GetStorageLayoutRequest;
import com.google.storage.control.v2.ListFoldersRequest;
import com.google.storage.control.v2.ListManagedFoldersRequest;
import com.google.storage.control.v2.StorageControlClient;
import com.google.storage.control.v2.StorageLayout;
import com.google.storage.control.v2.StorageLayoutName;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.slf4j.LoggerFactory;

public final class BucketCleaner {

private static final Logger LOGGER = Logger.getLogger(BucketCleaner.class.getName());
private static final org.slf4j.Logger LOGGER1 = LoggerFactory.getLogger(BucketCleaner.class);

public static void doCleanup(String bucketName, Storage s) {
LOGGER.fine("Starting bucket cleanup...");
Expand All @@ -48,12 +69,9 @@ public static void doCleanup(String bucketName, Storage s) {
b.getName(),
s.delete(b.getBlobId(), BlobSourceOption.userProject(projectId))))
.collect(Collectors.toList());
List<DeleteResult> failedDeletes =
deleteResults.stream().filter(r -> !r.success).collect(Collectors.toList());
failedDeletes.forEach(
r -> LOGGER.warning(String.format("Failed to delete object %s/%s", bucketName, r.name)));
boolean anyFailedObjectDeletes = getIfAnyFailedAndReport(bucketName, deleteResults, "object");

if (failedDeletes.isEmpty()) {
if (!anyFailedObjectDeletes) {
s.delete(bucketName, BucketSourceOption.userProject(projectId));
} else {
LOGGER.warning("Unable to delete bucket due to previous failed object deletes");
Expand All @@ -64,6 +82,163 @@ public static void doCleanup(String bucketName, Storage s) {
}
}

public static void doCleanup(String bucketName, Storage s, StorageControlClient ctrl) {
LOGGER.warning("Starting bucket cleanup: " + bucketName);
String projectId = s.getOptions().getProjectId();
try {
// TODO: probe bucket existence, a bad test could have deleted the bucket
Page<Blob> page1 =
s.list(
bucketName,
BlobListOption.userProject(projectId),
BlobListOption.versions(true),
BlobListOption.fields(BlobField.NAME));

List<DeleteResult> objectResults =
StreamSupport.stream(page1.iterateAll().spliterator(), false)
.map(
b ->
new DeleteResult(
b.getName(),
s.delete(b.getBlobId(), BlobSourceOption.userProject(projectId))))
.collect(Collectors.toList());
boolean anyFailedObjectDelete = getIfAnyFailedAndReport(bucketName, objectResults, "object");
boolean anyFailedFolderDelete = false;
boolean anyFailedManagedFolderDelete = false;

GrpcCallContext grpcCallContext =
GrpcCallContext.createDefault()
.withExtraHeaders(
ImmutableMap.of("x-goog-user-project", ImmutableList.of(projectId)));
if (!anyFailedObjectDelete) {
BucketName parent = BucketName.of("_", bucketName);
StorageLayout storageLayout =
ctrl.getStorageLayoutCallable()
.call(
GetStorageLayoutRequest.newBuilder()
.setName(
StorageLayoutName.of(parent.getProject(), parent.getBucket())
.toString())
.build(),
grpcCallContext);

List<DeleteResult> folderDeletes;
if (storageLayout.hasHierarchicalNamespace()
&& storageLayout.getHierarchicalNamespace().getEnabled()) {
folderDeletes =
StreamSupport.stream(
ctrl.listFoldersPagedCallable()
.call(
ListFoldersRequest.newBuilder().setParent(parent.toString()).build(),
grpcCallContext)
.iterateAll()
.spliterator(),
false)
.collect(Collectors.toList())
.stream()
.sorted(Collections.reverseOrder(Comparator.comparing(Folder::getName)))
.map(
folder -> {
String formatted = String.format("folder = %s", folder.getName());
LOGGER.warning(formatted);
boolean success = true;
try {
ctrl.deleteFolderCallable()
.call(
DeleteFolderRequest.newBuilder()
.setName(folder.getName())
.build(),
grpcCallContext);
} catch (ApiException e) {
LOGGER1.debug("{}", formatted, e);
success = false;
}
return new DeleteResult(folder.getName(), success);
})
.collect(Collectors.toList());
} else {
folderDeletes = ImmutableList.of();
}

List<DeleteResult> managedFolderDeletes;
try {
managedFolderDeletes =
StreamSupport.stream(
ctrl.listManagedFoldersPagedCallable()
.call(
ListManagedFoldersRequest.newBuilder()
.setParent(parent.toString())
.build(),
grpcCallContext)
.iterateAll()
.spliterator(),
false)
.map(
managedFolder -> {
String formatted =
String.format("managedFolder = %s", managedFolder.getName());
LOGGER.warning(formatted);
boolean success = true;
try {
ctrl.deleteManagedFolderCallable()
.call(
DeleteManagedFolderRequest.newBuilder()
.setName(managedFolder.getName())
.build(),
grpcCallContext);
} catch (ApiException e) {
LOGGER1.debug("{}", formatted, e);
success = false;
}
return new DeleteResult(managedFolder.getName(), success);
})
.collect(Collectors.toList());
} catch (FailedPreconditionException fpe) {
// FAILED_PRECONDITION: Uniform bucket-level access is required to be enabled on the
// bucket in order to perform this operation. Read more at
// https://cloud.google.com/storage/docs/uniform-bucket-level-access
managedFolderDeletes = ImmutableList.of();
}

anyFailedFolderDelete = getIfAnyFailedAndReport(bucketName, folderDeletes, "folder");
anyFailedManagedFolderDelete =
getIfAnyFailedAndReport(bucketName, managedFolderDeletes, "managed folder");
}

List<String> failed =
Stream.of(
anyFailedObjectDelete ? "object" : "",
anyFailedFolderDelete ? "folder" : "",
anyFailedManagedFolderDelete ? "managed folder" : "")
.filter(ss -> !ss.isEmpty())
.collect(Collectors.toList());

if (!anyFailedObjectDelete && !anyFailedFolderDelete && !anyFailedManagedFolderDelete) {
s.delete(bucketName, BucketSourceOption.userProject(projectId));
} else {
LOGGER.warning(
String.format(
"Unable to delete bucket %s due to previous failed %s deletes",
bucketName, failed));
}

LOGGER.warning("Bucket cleanup complete: " + bucketName);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, e, () -> "Error during bucket cleanup.");
}
}

private static boolean getIfAnyFailedAndReport(
String bucketName, List<DeleteResult> deleteResults, String resourceType) {
List<DeleteResult> failedDeletes =
deleteResults.stream().filter(r -> !r.success).collect(Collectors.toList());
failedDeletes.forEach(
r ->
LOGGER.warning(
String.format("Failed to delete %s %s/%s", resourceType, bucketName, r.name)));
return !failedDeletes.isEmpty();
}

private static final class DeleteResult {
private final String name;
private final boolean success;
Expand Down
Loading
Loading