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

feat: table deletion protection #1489

Closed
wants to merge 15 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -1366,15 +1366,7 @@ private String getTableName(String tableId) {
*/
private static ApiFuture<Table> transformToTableResponse(
ApiFuture<com.google.bigtable.admin.v2.Table> future) {
return ApiFutures.transform(
future,
new ApiFunction<com.google.bigtable.admin.v2.Table, Table>() {
@Override
public Table apply(com.google.bigtable.admin.v2.Table table) {
return Table.fromProto(table);
}
},
MoreExecutors.directExecutor());
return ApiFutures.transform(future, Table::fromProto, MoreExecutors.directExecutor());
}

/** Helper method to transform ApiFuture<Empty> to ApiFuture<Void> */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ public CreateTableRequest addChangeStreamRetention(Duration retention) {
return this;
}

public CreateTableRequest setDeletionProtection(boolean deletionProtection) {
requestBuilder.getTableBuilder().setDeletionProtection(deletionProtection);
return this;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public com.google.bigtable.admin.v2.Table.ClusterState.ReplicationState toProto(
private final String instanceId;
private final Map<String, ReplicationState> replicationStatesByClusterId;
private final List<ColumnFamily> columnFamilies;
private final boolean deletionProtection;

private final Duration changeStreamRetention;

Expand Down Expand Up @@ -135,19 +136,22 @@ public static Table fromProto(@Nonnull com.google.bigtable.admin.v2.Table proto)
TableName.parse(proto.getName()),
replicationStates.build(),
columnFamilies.build(),
changeStreamConfig);
changeStreamConfig,
proto.getDeletionProtection());
}

private Table(
TableName tableName,
Map<String, ReplicationState> replicationStatesByClusterId,
List<ColumnFamily> columnFamilies,
Duration changeStreamRetention) {
Duration changeStreamRetention,
boolean deletionProtection) {
this.instanceId = tableName.getInstance();
this.id = tableName.getTable();
this.replicationStatesByClusterId = replicationStatesByClusterId;
this.columnFamilies = columnFamilies;
this.changeStreamRetention = changeStreamRetention;
this.deletionProtection = deletionProtection;
}

/** Gets the table's id. */
Expand All @@ -172,6 +176,10 @@ public Duration getChangeStreamRetention() {
return changeStreamRetention;
}

public boolean isProtected() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: isDeletionProtected

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return deletionProtection;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -185,12 +193,18 @@ public boolean equals(Object o) {
&& Objects.equal(instanceId, table.instanceId)
&& Objects.equal(replicationStatesByClusterId, table.replicationStatesByClusterId)
&& Objects.equal(columnFamilies, table.columnFamilies)
&& Objects.equal(changeStreamRetention, table.changeStreamRetention);
&& Objects.equal(changeStreamRetention, table.changeStreamRetention)
&& Objects.equal(deletionProtection, table.deletionProtection);
}

@Override
public int hashCode() {
return Objects.hashCode(
id, instanceId, replicationStatesByClusterId, columnFamilies, changeStreamRetention);
id,
instanceId,
replicationStatesByClusterId,
columnFamilies,
changeStreamRetention,
deletionProtection);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.google.bigtable.admin.v2.ChangeStreamConfig;
import com.google.cloud.bigtable.admin.v2.internal.NameUtil;
import com.google.common.base.Preconditions;
import com.google.protobuf.util.FieldMaskUtil;
import java.util.Objects;
import org.threeten.bp.Duration;

Expand All @@ -30,6 +31,7 @@
*
* <ul>
* <li>Change stream retention period.
* <li>Table deletion protection
* </ul>
*/
public class UpdateTableRequest {
Expand Down Expand Up @@ -74,6 +76,17 @@ public UpdateTableRequest disableChangeStreamRetention() {
return addChangeStreamRetention(Duration.ZERO);
}

/** Update the table's deletion protection setting * */
public UpdateTableRequest setDeletionProtection(boolean deletionProtection) {
requestBuilder.setUpdateMask(
FieldMaskUtil.union(
requestBuilder.getUpdateMask(),
FieldMaskUtil.fromString(
com.google.bigtable.admin.v2.Table.class, "deletion_protection")));
requestBuilder.getTableBuilder().setDeletionProtection(deletionProtection);
return this;
}

@InternalApi
public com.google.bigtable.admin.v2.UpdateTableRequest toProto(
String projectId, String instanceId) {
Expand All @@ -85,14 +98,18 @@ public com.google.bigtable.admin.v2.UpdateTableRequest toProto(

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UpdateTableRequest)) return false;
if (this == o) {
return true;
}
if (!(o instanceof UpdateTableRequest)) {
return false;
}
UpdateTableRequest that = (UpdateTableRequest) o;
return Objects.equals(requestBuilder, that.requestBuilder);
return Objects.equals(requestBuilder.build(), that.requestBuilder.build());
}

@Override
public int hashCode() {
return Objects.hash(requestBuilder);
return Objects.hash(requestBuilder.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,36 @@ public void testUpdateTable() {
.isEqualTo(org.threeten.bp.Duration.ofHours(24));
}

@Test
public void testCreateTableWithDeletionProtection() {
// Setup
Mockito.when(mockStub.createTableCallable()).thenReturn(mockCreateTableCallable);

com.google.bigtable.admin.v2.CreateTableRequest expectedRequest =
com.google.bigtable.admin.v2.CreateTableRequest.newBuilder()
.setTable(
com.google.bigtable.admin.v2.Table.newBuilder().setDeletionProtection(true).build())
.setParent(INSTANCE_NAME)
.setTableId(TABLE_ID)
.build();

com.google.bigtable.admin.v2.Table expectedResponse =
com.google.bigtable.admin.v2.Table.newBuilder()
.setName(TABLE_NAME)
.setDeletionProtection(true)
.build();

Mockito.when(mockCreateTableCallable.futureCall(expectedRequest))
.thenReturn(ApiFutures.immediateFuture(expectedResponse));

// Execute
Table result =
adminClient.createTable(CreateTableRequest.of(TABLE_ID).setDeletionProtection(true));

// Verify
assertThat(result).isEqualTo(Table.fromProto(expectedResponse));
}

@Test
public void testModifyFamilies() {
// Setup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.api.gax.rpc.FailedPreconditionException;
import com.google.api.gax.rpc.NotFoundException;
import com.google.cloud.Policy;
import com.google.cloud.bigtable.admin.v2.BigtableTableAdminClient;
Expand All @@ -36,11 +38,13 @@
import com.google.cloud.bigtable.admin.v2.models.ModifyColumnFamiliesRequest;
import com.google.cloud.bigtable.admin.v2.models.Table;
import com.google.cloud.bigtable.admin.v2.models.UpdateTableRequest;
import com.google.cloud.bigtable.test_helpers.env.CloudEnv;
import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv;
import com.google.cloud.bigtable.test_helpers.env.PrefixGenerator;
import com.google.cloud.bigtable.test_helpers.env.TestEnvRule;
import com.google.common.collect.Maps;
import com.google.protobuf.ByteString;
import io.grpc.StatusRuntimeException;
import java.util.List;
import java.util.Map;
import org.junit.After;
Expand Down Expand Up @@ -69,6 +73,12 @@ public void setUp() {
@After
public void tearDown() {
try {
if (testEnvRule.env() instanceof CloudEnv) {
testEnvRule
.env()
.getTableAdminClient()
.updateTable(UpdateTableRequest.of(tableId).setDeletionProtection(false));
}
testEnvRule.env().getTableAdminClient().deleteTable(tableId);
} catch (NotFoundException e) {
// table was deleted in test or was never created. Carry on
Expand Down Expand Up @@ -206,6 +216,58 @@ public void listTables() {
assertFalse("List tables did not return any tables", tables.isEmpty());
}

@Test
public void createTableWithDeletionProtection() {
assume()
.withMessage("Emulator doesn't return proper responses for CreateTable")
.that(testEnvRule.env())
.isNotInstanceOf(EmulatorEnv.class);

Table table =
tableAdmin.createTable(CreateTableRequest.of(tableId).setDeletionProtection(true));
assertThat(table.isProtected()).isTrue();
}

@Test
public void updateTableWithDeletionProtection() {
assume()
.withMessage("Emulator doesn't return proper responses for CreateTable")
.that(testEnvRule.env())
.isNotInstanceOf(EmulatorEnv.class);

Table table = tableAdmin.createTable(CreateTableRequest.of(tableId));
assertThat(table.isProtected()).isFalse();

UpdateTableRequest request = UpdateTableRequest.of(tableId).setDeletionProtection(true);
Table updatedTable = tableAdmin.updateTable(request);
assertThat(updatedTable.getId()).isEqualTo(tableId);
assertThat(updatedTable.isProtected()).isTrue();

UpdateTableRequest request2 = UpdateTableRequest.of(tableId).setDeletionProtection(false);
Table updatedTable2 = tableAdmin.updateTable(request2);
assertThat(updatedTable2.getId()).isEqualTo(tableId);
assertThat(updatedTable2.isProtected()).isFalse();
}

@Test
public void deleteTableWithDeletionProtectionThrows() {
assume()
.withMessage("Emulator doesn't return proper responses for CreateTable")
.that(testEnvRule.env())
.isNotInstanceOf(EmulatorEnv.class);

Table table =
tableAdmin.createTable(CreateTableRequest.of(tableId).setDeletionProtection(true));
assertThat(table.isProtected()).isTrue();

try {
tableAdmin.deleteTable(tableId);
fail("should not have been able to delete table");
} catch (FailedPreconditionException e) {
assertThat(e.getCause()).isInstanceOf(StatusRuntimeException.class);
}
}

@Test
public void listTablesAsync() throws Exception {
tableAdmin.createTable(CreateTableRequest.of(tableId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public void testFromProto() {
.setSeconds(1)
.setNanos(99)))
.build())
.setDeletionProtection(true)
.build();

Table result = Table.fromProto(proto);
Expand All @@ -85,6 +86,8 @@ public void testFromProto() {
com.google.cloud.bigtable.admin.v2.models.ColumnFamily.fromProto(
entry.getKey(), entry.getValue()));
}

assertThat(result.isProtected()).isTrue();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,37 @@ public void testNoChangeChangeStreamToProto() {
.build();
assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto);
}

@Test
public void testToProto() {
UpdateTableRequest request = UpdateTableRequest.of(TABLE_ID).setDeletionProtection(true);
com.google.bigtable.admin.v2.UpdateTableRequest requestProto =
com.google.bigtable.admin.v2.UpdateTableRequest.newBuilder()
.setTable(
Table.newBuilder()
.setName(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID))
.setDeletionProtection(true))
.setUpdateMask(FieldMask.newBuilder().addPaths("deletion_protection"))
.build();

assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto);
}

@Test
public void testEquality() {
UpdateTableRequest request = UpdateTableRequest.of(TABLE_ID).setDeletionProtection(true);

assertThat(request).isEqualTo(UpdateTableRequest.of(TABLE_ID).setDeletionProtection(true));
assertThat(request).isNotEqualTo(UpdateTableRequest.of(TABLE_ID).setDeletionProtection(false));
}

@Test
public void testHashCode() {
UpdateTableRequest request = UpdateTableRequest.of(TABLE_ID).setDeletionProtection(true);

assertThat(request.hashCode())
.isEqualTo(UpdateTableRequest.of(TABLE_ID).setDeletionProtection(true).hashCode());
assertThat(request.hashCode())
.isNotEqualTo(UpdateTableRequest.of(TABLE_ID).setDeletionProtection(false).hashCode());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
* <li>{@code bigtable.table}
* </ul>
*/
class CloudEnv extends AbstractTestEnv {
public class CloudEnv extends AbstractTestEnv {
private static final Predicate<InetSocketAddress> DIRECT_PATH_IPV6_MATCHER =
new Predicate<InetSocketAddress>() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.google.cloud.bigtable.admin.v2.models.AppProfile;
import com.google.cloud.bigtable.admin.v2.models.Cluster;
import com.google.cloud.bigtable.admin.v2.models.Instance;
import com.google.cloud.bigtable.admin.v2.models.UpdateTableRequest;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
Expand Down Expand Up @@ -204,6 +205,11 @@ private void cleanupStaleTables(String stalePrefix) {
}
if (stalePrefix.compareTo(tableId) > 0) {
try {
if (env() instanceof CloudEnv) {
env()
.getTableAdminClient()
.updateTable(UpdateTableRequest.of(tableId).setDeletionProtection(false));
}
env().getTableAdminClient().deleteTable(tableId);
} catch (NotFoundException ignored) {

Expand Down