diff --git a/.kokoro/nightly/integration.cfg b/.kokoro/nightly/integration.cfg index 6f27f37c2..c203120c9 100644 --- a/.kokoro/nightly/integration.cfg +++ b/.kokoro/nightly/integration.cfg @@ -8,7 +8,7 @@ env_vars: { env_vars: { key: "INTEGRATION_TEST_ARGS" - value: "-P bigtable-emulator-it,bigtable-prod-it -Dbigtable.project=gcloud-devel -Dbigtable.instance=google-cloud-bigtable -Dbigtable.table=integration-tests" + value: "-P bigtable-emulator-it,bigtable-prod-it -Dbigtable.project=gcloud-devel -Dbigtable.instance=google-cloud-bigtable -Dbigtable.table=integration-tests -Dbigtable.kms_key_name=projects/gcloud-devel/locations/us-central1/keyRings/cmek-test-key-ring/cryptoKeys/cmek-test-key -Dbigtable.wait-for-cmek-key-status=true" } env_vars: { diff --git a/.kokoro/presubmit/integration.cfg b/.kokoro/presubmit/integration.cfg index 850ea7e57..7654f56c0 100644 --- a/.kokoro/presubmit/integration.cfg +++ b/.kokoro/presubmit/integration.cfg @@ -8,7 +8,7 @@ env_vars: { env_vars: { key: "INTEGRATION_TEST_ARGS" - value: "-P bigtable-emulator-it,bigtable-prod-it -Dbigtable.project=gcloud-devel -Dbigtable.instance=google-cloud-bigtable -Dbigtable.table=integration-tests" + value: "-P bigtable-emulator-it,bigtable-prod-it -Dbigtable.project=gcloud-devel -Dbigtable.instance=google-cloud-bigtable -Dbigtable.table=integration-tests -Dbigtable.kms_key_name=projects/gcloud-devel/locations/us-central1/keyRings/cmek-test-key-ring/cryptoKeys/cmek-test-key -Dbigtable.wait-for-cmek-key-status=true" } env_vars: { diff --git a/google-cloud-bigtable/pom.xml b/google-cloud-bigtable/pom.xml index 43b22352c..3e3ba88c5 100644 --- a/google-cloud-bigtable/pom.xml +++ b/google-cloud-bigtable/pom.xml @@ -214,6 +214,11 @@ truth test + + com.google.truth.extensions + truth-proto-extension + test + io.grpc grpc-testing diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java index d4029e19b..b8515ea17 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java @@ -30,6 +30,8 @@ import com.google.bigtable.admin.v2.ListBackupsRequest; import com.google.bigtable.admin.v2.ListTablesRequest; import com.google.bigtable.admin.v2.RestoreTableMetadata; +import com.google.bigtable.admin.v2.Table.ClusterState; +import com.google.bigtable.admin.v2.Table.View; import com.google.cloud.Policy; import com.google.cloud.Policy.DefaultMarshaller; import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListBackupsPage; @@ -40,6 +42,7 @@ import com.google.cloud.bigtable.admin.v2.models.Backup; import com.google.cloud.bigtable.admin.v2.models.CreateBackupRequest; import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; +import com.google.cloud.bigtable.admin.v2.models.EncryptionInfo; import com.google.cloud.bigtable.admin.v2.models.GCRules; import com.google.cloud.bigtable.admin.v2.models.ModifyColumnFamiliesRequest; import com.google.cloud.bigtable.admin.v2.models.OptimizeRestoredTableOperationToken; @@ -49,6 +52,8 @@ import com.google.cloud.bigtable.admin.v2.models.UpdateBackupRequest; import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.util.concurrent.MoreExecutors; import com.google.iam.v1.GetIamPolicyRequest; @@ -60,6 +65,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import javax.annotation.Nonnull; @@ -519,6 +525,52 @@ private ApiFuture getTableAsync( return transformToTableResponse(this.stub.getTableCallable().futureCall(request)); } + /** + * Gets the current encryption info for the table across all of the clusters. + * + *

The returned Map will be keyed by cluster id and contain a status for all of the keys in + * use. + */ + public Map> getEncryptionInfo(String tableId) { + return ApiExceptions.callAndTranslateApiException(getEncryptionInfoAsync(tableId)); + } + + /** + * Asynchronously gets the current encryption info for the table across all of the clusters. + * + *

The returned Map will be keyed by cluster id and contain a status for all of the keys in + * use. + */ + public ApiFuture>> getEncryptionInfoAsync(String tableId) { + GetTableRequest request = + GetTableRequest.newBuilder() + .setName(getTableName(tableId)) + .setView(View.ENCRYPTION_VIEW) + .build(); + return ApiFutures.transform( + this.stub.getTableCallable().futureCall(request), + new ApiFunction>>() { + @Override + public Map> apply(com.google.bigtable.admin.v2.Table table) { + ImmutableMap.Builder> result = ImmutableMap.builder(); + + for (Map.Entry entry : table.getClusterStatesMap().entrySet()) { + ImmutableList.Builder infos = ImmutableList.builder(); + + for (com.google.bigtable.admin.v2.EncryptionInfo infoProto : + entry.getValue().getEncryptionInfoList()) { + infos.add(EncryptionInfo.fromProto(infoProto)); + } + + result.put(entry.getKey(), infos.build()); + } + + return result.build(); + } + }, + MoreExecutors.directExecutor()); + } + /** * Lists all table IDs in the instance. * diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/Backup.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/Backup.java index 54002da63..ce0ed7efc 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/Backup.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/Backup.java @@ -142,6 +142,18 @@ public State getState() { return State.fromProto(proto.getState()); } + /** + * Get the encryption information for the backup. + * + *

If encryption_type is CUSTOMER_MANAGED_ENCRYPTION, kms_key_version will be filled in with + * status UNKNOWN. + * + *

If encryption_type is GOOGLE_DEFAULT_ENCRYPTION, all other fields will have default value. + */ + public EncryptionInfo getEncryptionInfo() { + return EncryptionInfo.fromProto(proto.getEncryptionInfo()); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/Cluster.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/Cluster.java index 1d9d695ce..cf7218b06 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/Cluster.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/Cluster.java @@ -30,6 +30,7 @@ * in the instance. */ public class Cluster { + public enum State { /** The state of the cluster could not be determined. */ NOT_KNOWN(com.google.bigtable.admin.v2.Cluster.State.STATE_NOT_KNOWN), @@ -156,6 +157,18 @@ public StorageType getStorageType() { return StorageType.fromProto(stateProto.getDefaultStorageType()); } + /** + * Google Cloud Key Management Service (KMS) settings for a CMEK-protected Bigtable cluster. This + * returns the full resource name of the Cloud KMS key in the format + * `projects/{key_project_id}/locations/{location}/keyRings/{ring_name}/cryptoKeys/{key_name}` + */ + public String getKmsKeyName() { + if (stateProto.hasEncryptionConfig()) { + return stateProto.getEncryptionConfig().getKmsKeyName(); + } + return null; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateClusterRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateClusterRequest.java index 4711097d5..cf06f63b0 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateClusterRequest.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateClusterRequest.java @@ -49,6 +49,7 @@ * details */ public final class CreateClusterRequest { + private final com.google.bigtable.admin.v2.CreateClusterRequest.Builder proto = com.google.bigtable.admin.v2.CreateClusterRequest.newBuilder(); // instanceId and zone are short ids, which will be expanded to full names when the project name @@ -104,6 +105,17 @@ public CreateClusterRequest setStorageType(@Nonnull StorageType storageType) { return this; } + /** + * Sets the Google Cloud Key Management Service (KMS) key for a CMEK-protected Bigtable. This + * requires the full resource name of the Cloud KMS key, in the format + * `projects/{key_project_id}/locations/{location}/keyRings/{ring_name}/cryptoKeys/{key_name}` + */ + public CreateClusterRequest setKmsKeyName(@Nonnull String kmsKeyName) { + Preconditions.checkNotNull(kmsKeyName); + proto.getClusterBuilder().getEncryptionConfigBuilder().setKmsKeyName(kmsKeyName); + return this; + } + /** * Creates the request protobuf. This method is considered an internal implementation detail and * not meant to be used by applications. diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateInstanceRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateInstanceRequest.java index 467344c82..b318c95ca 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateInstanceRequest.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateInstanceRequest.java @@ -147,6 +147,35 @@ public CreateInstanceRequest addCluster( return this; } + /** + * Adds a CMEK protected cluster using the specified KMS key name. + * + * @param clusterId the name of the cluster. + * @param zone the zone where the cluster will be created. + * @param serveNodes the number of nodes that cluster will contain. DEVELOPMENT instance clusters + * must have exactly one node. + * @param storageType the type of storage used by this cluster to serve its parent instance's + * tables. + * @param kmsKeyName the full name of the KMS key name to use in the format + * `projects/{key_project_id}/locations/{location}/keyRings/{ring_name}/cryptoKeys/{key_name}` + */ + public CreateInstanceRequest addCmekCluster( + @Nonnull String clusterId, + @Nonnull String zone, + int serveNodes, + @Nonnull StorageType storageType, + @Nonnull String kmsKeyName) { + CreateClusterRequest clusterRequest = + CreateClusterRequest.of("ignored-instance-id", clusterId) + .setZone(zone) + .setServeNodes(serveNodes) + .setStorageType(storageType) + .setKmsKeyName(kmsKeyName); + clusterRequests.add(clusterRequest); + + return this; + } + /** * Adds a DEVELOPMENT cluster to the instance request. * diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/EncryptionInfo.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/EncryptionInfo.java new file mode 100644 index 000000000..9eb3b13e1 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/EncryptionInfo.java @@ -0,0 +1,108 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.admin.v2.models; + +import com.google.bigtable.admin.v2.EncryptionInfo.EncryptionType; +import com.google.cloud.bigtable.common.Status; +import com.google.common.base.Objects; + +/** + * Encryption information for a given resource. + * + *

If this resource is protected with customer managed encryption, the in-use Google Cloud Key + * Management Service (KMS) key versions will be specified along with their status. + */ +public final class EncryptionInfo { + public enum Type { + /** Encryption type was not specified, though data at rest remains encrypted. */ + ENCRYPTION_TYPE_UNSPECIFIED( + com.google.bigtable.admin.v2.EncryptionInfo.EncryptionType.ENCRYPTION_TYPE_UNSPECIFIED), + /** + * The data backing this resource is encrypted at rest with a key that is fully managed by + * Google. No key version or status will be populated. + */ + GOOGLE_DEFAULT_ENCRYPTION( + com.google.bigtable.admin.v2.EncryptionInfo.EncryptionType.GOOGLE_DEFAULT_ENCRYPTION), + /** + * The data backing this resource is encrypted at rest with a key that is fully managed by + * Google. No key version or status will be populated. This is the default state. + */ + CUSTOMER_MANAGED_ENCRYPTION( + com.google.bigtable.admin.v2.EncryptionInfo.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION), + /** Type not known by the client, please upgrade your client */ + UNRECOGNIZED(com.google.bigtable.admin.v2.EncryptionInfo.EncryptionType.UNRECOGNIZED); + + private final com.google.bigtable.admin.v2.EncryptionInfo.EncryptionType proto; + + Type(EncryptionType proto) { + this.proto = proto; + } + + /** Wraps the EncryptionInfo protobuf. */ + public static Type fromProto(com.google.bigtable.admin.v2.EncryptionInfo.EncryptionType proto) { + for (Type type : values()) { + if (Objects.equal(type.proto, proto)) { + return type; + } + } + return UNRECOGNIZED; + } + } + + private com.google.bigtable.admin.v2.EncryptionInfo proto; + + public static EncryptionInfo fromProto(com.google.bigtable.admin.v2.EncryptionInfo proto) { + return new EncryptionInfo(proto); + } + + private EncryptionInfo(com.google.bigtable.admin.v2.EncryptionInfo proto) { + this.proto = proto; + } + + public Type getType() { + return EncryptionInfo.Type.fromProto(proto.getEncryptionType()); + } + + public String getKmsKeyVersion() { + return proto.getKmsKeyVersion(); + } + + public Status getStatus() { + return Status.fromProto(proto.getEncryptionStatus()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EncryptionInfo that = (EncryptionInfo) o; + return Objects.equal(proto, that.proto); + } + + @Override + public int hashCode() { + return Objects.hashCode(proto); + } + + @Override + public String toString() { + return proto.toString(); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/common/Status.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/common/Status.java new file mode 100644 index 000000000..83f7c188d --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/common/Status.java @@ -0,0 +1,127 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.common; + +import com.google.common.base.Objects; + +/** + * The `Status` type defines a logical error model. Each `Status` message contains an error code and + * a error message. + * + *

This primarily wraps the protobuf {@link com.google.rpc.Status}. + */ +public final class Status { + public enum Code { + OK(com.google.rpc.Code.OK), + CANCELLED(com.google.rpc.Code.CANCELLED), + UNKNOWN(com.google.rpc.Code.UNKNOWN), + INVALID_ARGUMENT(com.google.rpc.Code.INVALID_ARGUMENT), + DEADLINE_EXCEEDED(com.google.rpc.Code.DEADLINE_EXCEEDED), + NOT_FOUND(com.google.rpc.Code.NOT_FOUND), + ALREADY_EXISTS(com.google.rpc.Code.ALREADY_EXISTS), + PERMISSION_DENIED(com.google.rpc.Code.PERMISSION_DENIED), + UNAUTHENTICATED(com.google.rpc.Code.UNAUTHENTICATED), + RESOURCE_EXHAUSTED(com.google.rpc.Code.RESOURCE_EXHAUSTED), + FAILED_PRECONDITION(com.google.rpc.Code.FAILED_PRECONDITION), + ABORTED(com.google.rpc.Code.ABORTED), + OUT_OF_RANGE(com.google.rpc.Code.OUT_OF_RANGE), + UNIMPLEMENTED(com.google.rpc.Code.UNIMPLEMENTED), + INTERNAL(com.google.rpc.Code.INTERNAL), + UNAVAILABLE(com.google.rpc.Code.UNAVAILABLE), + DATA_LOSS(com.google.rpc.Code.DATA_LOSS), + + /** Code not known by the client, please upgrade your client */ + UNRECOGNIZED(com.google.rpc.Code.UNRECOGNIZED); + + private final com.google.rpc.Code proto; + + public static Code fromProto(com.google.rpc.Code proto) { + for (Code code : values()) { + if (code.proto.equals(proto)) { + return code; + } + } + return UNRECOGNIZED; + } + + public static Code fromCodeNumber(int num) { + for (Code code : values()) { + if (code.proto == com.google.rpc.Code.UNRECOGNIZED) { + continue; + } + if (code.proto.getNumber() == num) { + return code; + } + } + return UNRECOGNIZED; + } + + Code(com.google.rpc.Code proto) { + this.proto = proto; + } + + public com.google.rpc.Code toProto() { + return proto; + } + } + + private final com.google.rpc.Status proto; + + /** Wraps the given protobuf Status */ + public static Status fromProto(com.google.rpc.Status proto) { + return new Status(proto); + } + + private Status(com.google.rpc.Status proto) { + this.proto = proto; + } + + /** Gets the typesafe code. */ + public Code getCode() { + return Code.fromCodeNumber(proto.getCode()); + } + + /** Gets error message. */ + public String getMessage() { + return proto.getMessage(); + } + + /** Gets the underlying protobuf. */ + public com.google.rpc.Status toProto() { + return proto; + } + + public String toString() { + return proto.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Status status = (Status) o; + return Objects.equal(proto, status.proto); + } + + @Override + public int hashCode() { + return Objects.hashCode(proto); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTest.java index eeb87ada2..adc7ae052 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTest.java @@ -42,6 +42,7 @@ import com.google.bigtable.admin.v2.ModifyColumnFamiliesRequest.Modification; import com.google.bigtable.admin.v2.RestoreSourceType; import com.google.bigtable.admin.v2.RestoreTableMetadata; +import com.google.bigtable.admin.v2.Table.ClusterState; import com.google.bigtable.admin.v2.Table.View; import com.google.bigtable.admin.v2.TableName; import com.google.cloud.Identity; @@ -55,12 +56,14 @@ import com.google.cloud.bigtable.admin.v2.models.Backup; import com.google.cloud.bigtable.admin.v2.models.CreateBackupRequest; import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; +import com.google.cloud.bigtable.admin.v2.models.EncryptionInfo; import com.google.cloud.bigtable.admin.v2.models.ModifyColumnFamiliesRequest; import com.google.cloud.bigtable.admin.v2.models.RestoreTableRequest; import com.google.cloud.bigtable.admin.v2.models.RestoredTableResult; import com.google.cloud.bigtable.admin.v2.models.Table; import com.google.cloud.bigtable.admin.v2.models.UpdateBackupRequest; import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.io.BaseEncoding; import com.google.longrunning.Operation; @@ -72,6 +75,7 @@ import io.grpc.Status; import io.grpc.Status.Code; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Before; @@ -300,6 +304,45 @@ public void testGetTable() { assertThat(actualResult).isEqualTo(Table.fromProto(expectedResponse)); } + @Test + public void testGetEncryptionInfos() { + // Setup + GetTableRequest expectedRequest = + GetTableRequest.newBuilder().setName(TABLE_NAME).setView(View.ENCRYPTION_VIEW).build(); + + com.google.bigtable.admin.v2.EncryptionInfo expectedEncryptionInfo = + com.google.bigtable.admin.v2.EncryptionInfo.newBuilder() + .setKmsKeyVersion("some key") + .setEncryptionType( + com.google.bigtable.admin.v2.EncryptionInfo.EncryptionType + .CUSTOMER_MANAGED_ENCRYPTION) + .setEncryptionStatus( + com.google.rpc.Status.newBuilder() + .setCode(Code.FAILED_PRECONDITION.value()) + .setMessage("something failed")) + .build(); + + com.google.bigtable.admin.v2.Table expectedResponse = + com.google.bigtable.admin.v2.Table.newBuilder() + .setName(TABLE_NAME) + .putClusterStates( + "cluster1", + ClusterState.newBuilder().addEncryptionInfo(expectedEncryptionInfo).build()) + .build(); + + Mockito.when(mockGetTableCallable.futureCall(expectedRequest)) + .thenReturn(ApiFutures.immediateFuture(expectedResponse)); + + // Execute + Map> actualResult = + adminClient.getEncryptionInfo(TABLE_ID); + + // Verify that the encryption info is transfered from the proto to the model. + assertThat(actualResult) + .containsExactly( + "cluster1", ImmutableList.of(EncryptionInfo.fromProto(expectedEncryptionInfo))); + } + @Test public void testListTables() { // Setup diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableCmekIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableCmekIT.java new file mode 100644 index 000000000..b3c9c4ce6 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableCmekIT.java @@ -0,0 +1,259 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.admin.v2.it; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; +import static org.junit.Assert.fail; + +import com.google.api.gax.rpc.ApiException; +import com.google.cloud.bigtable.admin.v2.BigtableInstanceAdminClient; +import com.google.cloud.bigtable.admin.v2.BigtableTableAdminClient; +import com.google.cloud.bigtable.admin.v2.internal.NameUtil; +import com.google.cloud.bigtable.admin.v2.models.Backup; +import com.google.cloud.bigtable.admin.v2.models.Cluster; +import com.google.cloud.bigtable.admin.v2.models.CreateBackupRequest; +import com.google.cloud.bigtable.admin.v2.models.CreateClusterRequest; +import com.google.cloud.bigtable.admin.v2.models.CreateInstanceRequest; +import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; +import com.google.cloud.bigtable.admin.v2.models.EncryptionInfo; +import com.google.cloud.bigtable.admin.v2.models.StorageType; +import com.google.cloud.bigtable.common.Status; +import com.google.cloud.bigtable.common.Status.Code; +import com.google.cloud.bigtable.test_helpers.env.AbstractTestEnv; +import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv; +import com.google.cloud.bigtable.test_helpers.env.TestEnvRule; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.threeten.bp.Instant; +import org.threeten.bp.temporal.ChronoUnit; + +/** + * Tests our CMEK offering. It can take up to 5 mins after a CMEK-protected table is created for the + * key version and status fields to be populated. Set the `bigtable.wait-for-cmek-key-status` system + * property to `true` when running the test in order to poll until the final state can be asserted. + */ +@RunWith(JUnit4.class) +public class BigtableCmekIT { + + private static final int[] BACKOFF_DURATION = {5, 10, 50, 100, 150, 200, 250, 300}; + private static final Logger LOGGER = Logger.getLogger(BigtableCmekIT.class.getName()); + private static final String TEST_TABLE_ID = "test-table-for-cmek-it"; + private static final String BACKUP_ID = "test-table-for-cmek-it-backup"; + + @ClassRule public static TestEnvRule testEnvRule = new TestEnvRule(); + + private static String instanceId; + private static String clusterId1; + private static String clusterId2; + private static String kmsKeyName; + private static String zoneId; + + private static BigtableInstanceAdminClient instanceAdmin; + private static BigtableTableAdminClient tableAdmin; + + @BeforeClass + public static void validatePlatform() throws IOException { + assume() + .withMessage("Emulator doesn't support CMEK") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + kmsKeyName = testEnvRule.env().getKmsKeyName(); + assertThat(kmsKeyName).isNotNull(); + assertThat(kmsKeyName).isNotEmpty(); + + instanceId = AbstractTestEnv.TEST_INSTANCE_PREFIX + Instant.now().getEpochSecond(); + clusterId1 = instanceId + "-c1"; + clusterId2 = instanceId + "-c2"; + zoneId = testEnvRule.env().getPrimaryZone(); + + instanceAdmin = testEnvRule.env().getInstanceAdminClient(); + tableAdmin = + BigtableTableAdminClient.create( + testEnvRule + .env() + .getTableAdminSettings() + .toBuilder() + .setInstanceId(instanceId) + .build()); + } + + @AfterClass + public static void teardown() { + if (tableAdmin != null) { + tableAdmin.close(); + } + if (instanceAdmin != null) { + instanceAdmin.close(); + } + } + + @Test + public void instanceAndClusterTest() { + try { + // With a KMS_KEY created and specified using `bigtable.kms_key_name` env variable, create a + // CMEK protected instance + instanceAdmin.createInstance( + CreateInstanceRequest.of(instanceId) + .addCmekCluster(clusterId1, zoneId, 1, StorageType.SSD, kmsKeyName)); + + // Keys are specified per-cluster with each cluster requesting the same key and the cluster's + // zone must be within the region of the key + Cluster cluster = instanceAdmin.getCluster(instanceId, clusterId1); + assertThat(cluster.getKmsKeyName()).isEqualTo(kmsKeyName); + + String secondZoneId = testEnvRule.env().getPrimaryRegionSecondZone(); + instanceAdmin.createCluster( + CreateClusterRequest.of(instanceId, clusterId2) + .setZone(secondZoneId) + .setServeNodes(1) + .setStorageType(StorageType.SSD) + .setKmsKeyName(kmsKeyName)); + + Cluster secondCluster = instanceAdmin.getCluster(instanceId, clusterId2); + assertThat(secondCluster.getKmsKeyName()).isEqualTo(kmsKeyName); + + final String nonPrimaryRegionZoneId = testEnvRule.env().getSecondaryZone(); + try { + instanceAdmin.createCluster( + CreateClusterRequest.of(instanceId, clusterId2) + .setZone(nonPrimaryRegionZoneId) + .setServeNodes(1) + .setStorageType(StorageType.SSD) + .setKmsKeyName(kmsKeyName)); + } catch (com.google.api.gax.rpc.FailedPreconditionException e) { + assertThat(e.getMessage()) + .contains( + "FAILED_PRECONDITION: Error in field 'cluster' : " + + "Error in field 'encryption_config.kms_key_name' : CMEK key " + + kmsKeyName + + " cannot be used to protect a cluster in zone " + + NameUtil.formatLocationName( + testEnvRule.env().getProjectId(), nonPrimaryRegionZoneId)); + } + } finally { + instanceAdmin.deleteInstance(instanceId); + } + } + + @Test + public void tableTest() throws Exception { + try { + instanceAdmin.createInstance( + CreateInstanceRequest.of(instanceId) + .addCmekCluster(clusterId1, zoneId, 1, StorageType.SSD, kmsKeyName)); + + // Create a table. Key is inherited from the cluster configuration + tableAdmin.createTable(CreateTableRequest.of(TEST_TABLE_ID).addFamily("cf")); + + // Confirm that table is CMEK-protected + if (testEnvRule.env().shouldWaitForCmekKeyStatusUpdate()) { + waitForCmekStatus(TEST_TABLE_ID, clusterId1); + } + Map> encryptionInfos = + tableAdmin.getEncryptionInfo(TEST_TABLE_ID); + assertThat(encryptionInfos).hasSize(1); + assertThat(encryptionInfos.get(clusterId1)).hasSize(1); + EncryptionInfo encryptionInfo = encryptionInfos.get(clusterId1).get(0); + assertThat(encryptionInfo.getType()) + .isEqualTo(EncryptionInfo.Type.CUSTOMER_MANAGED_ENCRYPTION); + assertThat(encryptionInfo.getStatus().getCode()).isAnyOf(Status.Code.OK, Status.Code.UNKNOWN); + if (testEnvRule.env().shouldWaitForCmekKeyStatusUpdate()) { + assertThat(encryptionInfo.getStatus().getCode()).isEqualTo(Status.Code.OK); + } + // For up to 5 minutes after a table is newly created, the key version and status fields are + // not + // populated. + // Set the `bigtable.wait-for-cmek-key-status` system property to `true` when running the test + // in order to poll until the final state can be asserted. + if (encryptionInfo.getStatus().getCode() == Code.UNKNOWN) { + assertThat(encryptionInfo.getKmsKeyVersion()).isEmpty(); + assertThat(encryptionInfo.getStatus().getMessage()) + .isEqualTo("Key version is not yet known."); + } else { + assertThat(encryptionInfo.getKmsKeyVersion()).startsWith(kmsKeyName); + assertThat(encryptionInfo.getStatus().getMessage()).isEqualTo(""); + } + } finally { + tableAdmin.deleteTable(TEST_TABLE_ID); + instanceAdmin.deleteInstance(instanceId); + } + } + + @Test + public void backupTest() { + try { + instanceAdmin.createInstance( + CreateInstanceRequest.of(instanceId) + .addCmekCluster(clusterId1, zoneId, 1, StorageType.SSD, kmsKeyName)); + tableAdmin.createTable(CreateTableRequest.of(TEST_TABLE_ID).addFamily("cf")); + + // Create a backup. + // Backups are pinned to the primary version of their table's CMEK key at the time they are + // taken + tableAdmin.createBackup( + CreateBackupRequest.of(clusterId1, BACKUP_ID) + .setExpireTime(Instant.now().plus(6, ChronoUnit.HOURS)) + .setSourceTableId(TEST_TABLE_ID)); + + Backup backup = tableAdmin.getBackup(clusterId1, BACKUP_ID); + + // Confirm encryption details for an existing backup + // The backup will be returned with the CMEK key version that the backup is pinned to. + // The status of that key version will always be UNKNOWN. + assertThat(backup.getEncryptionInfo().getKmsKeyVersion()).startsWith(kmsKeyName); + assertThat(backup.getEncryptionInfo().getStatus().getCode()).isEqualTo(Status.Code.UNKNOWN); + assertThat(backup.getEncryptionInfo().getType()) + .isEqualTo(EncryptionInfo.Type.CUSTOMER_MANAGED_ENCRYPTION); + assertThat(backup.getEncryptionInfo().getStatus().getMessage()) + .isEqualTo("Status of the associated key version is not tracked."); + } finally { + tableAdmin.deleteBackup(clusterId1, BACKUP_ID); + tableAdmin.deleteTable(TEST_TABLE_ID); + instanceAdmin.deleteInstance(instanceId); + } + } + + private void waitForCmekStatus(String tableId, String clusterId) throws InterruptedException { + for (int i = 0; i < BACKOFF_DURATION.length; i++) { + try { + EncryptionInfo encryptionInfo = tableAdmin.getEncryptionInfo(tableId).get(clusterId).get(0); + if (encryptionInfo.getStatus().getCode() == Code.OK) { + return; + } + } catch (ApiException ex) { + LOGGER.info( + "Wait for " + + BACKOFF_DURATION[i] + + " seconds for key status for table " + + tableId + + " and cluster " + + clusterId); + } + Thread.sleep(BACKOFF_DURATION[i] * 1000); + } + fail("CMEK key status failed to return"); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/BackupTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/BackupTest.java index be32058e2..fe73c5588 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/BackupTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/BackupTest.java @@ -17,9 +17,12 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.bigtable.admin.v2.EncryptionInfo.EncryptionType; +import com.google.cloud.bigtable.common.Status; import com.google.common.collect.Lists; import com.google.protobuf.Timestamp; import com.google.protobuf.util.Timestamps; +import com.google.rpc.Code; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -74,6 +77,34 @@ public void testFromProto() { assertThat(result.getState()).isEqualTo(Backup.State.READY); } + @Test + public void testFromProtoCmek() { + com.google.bigtable.admin.v2.Backup proto = + com.google.bigtable.admin.v2.Backup.newBuilder() + .setName("projects/my-project/instances/instance1/clusters/cluster1/backups/backup1") + .setSourceTable("projects/my-project/instances/instance1/tables/table1") + .setExpireTime(Timestamp.newBuilder().setSeconds(1234)) + .setStartTime(Timestamp.newBuilder().setSeconds(1234)) + .setEndTime(Timestamp.newBuilder().setSeconds(1234)) + .setSizeBytes(123456) + .setState(com.google.bigtable.admin.v2.Backup.State.READY) + .setEncryptionInfo( + com.google.bigtable.admin.v2.EncryptionInfo.newBuilder() + .setEncryptionType(EncryptionType.CUSTOMER_MANAGED_ENCRYPTION) + .setKmsKeyVersion("some key version") + .setEncryptionStatus( + com.google.rpc.Status.newBuilder().setCode(Code.OK.getNumber()).build()) + .build()) + .build(); + + Backup result = Backup.fromProto(proto); + + assertThat(result.getEncryptionInfo().getType()) + .isEqualTo(EncryptionInfo.Type.CUSTOMER_MANAGED_ENCRYPTION); + assertThat(result.getEncryptionInfo().getKmsKeyVersion()).isEqualTo("some key version"); + assertThat(result.getEncryptionInfo().getStatus().getCode()).isEqualTo(Status.Code.OK); + } + @Test public void testRequiresName() { com.google.bigtable.admin.v2.Backup proto = diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/ClusterTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/ClusterTest.java index 20a143e09..f2f217ab5 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/ClusterTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/ClusterTest.java @@ -17,6 +17,8 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.bigtable.admin.v2.Cluster.EncryptionConfig; +import com.google.bigtable.admin.v2.Cluster.State; import com.google.common.collect.Lists; import java.util.List; import org.junit.Test; @@ -32,7 +34,7 @@ public void testFromProto() { com.google.bigtable.admin.v2.Cluster.newBuilder() .setName("projects/my-project/instances/my-instance/clusters/my-cluster") .setLocation("projects/my-project/locations/us-east1-c") - .setState(com.google.bigtable.admin.v2.Cluster.State.READY) + .setState(State.READY) .setServeNodes(30) .setDefaultStorageType(com.google.bigtable.admin.v2.StorageType.SSD) .build(); @@ -45,6 +47,30 @@ public void testFromProto() { assertThat(result.getState()).isEqualTo(Cluster.State.READY); assertThat(result.getServeNodes()).isEqualTo(30); assertThat(result.getStorageType()).isEqualTo(StorageType.SSD); + assertThat(result.getKmsKeyName()).isEqualTo(null); + } + + @Test + public void testFromProtoCmek() { + com.google.bigtable.admin.v2.Cluster proto = + com.google.bigtable.admin.v2.Cluster.newBuilder() + .setName("projects/my-project/instances/my-instance/clusters/my-cluster") + .setLocation("projects/my-project/locations/us-east1-c") + .setState(State.READY) + .setServeNodes(30) + .setDefaultStorageType(com.google.bigtable.admin.v2.StorageType.SSD) + .setEncryptionConfig( + EncryptionConfig.newBuilder() + .setKmsKeyName( + "projects/my-project/locations/us-east1-c/keyRings/my-key-ring/cryptoKeys/my-key") + .build()) + .build(); + + Cluster result = Cluster.fromProto(proto); + + assertThat(result.getKmsKeyName()) + .isEqualTo( + "projects/my-project/locations/us-east1-c/keyRings/my-key-ring/cryptoKeys/my-key"); } @Test diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateClusterRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateClusterRequestTest.java index 17152d546..566641039 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateClusterRequestTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateClusterRequestTest.java @@ -16,7 +16,10 @@ package com.google.cloud.bigtable.admin.v2.models; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import com.google.bigtable.admin.v2.Cluster; +import com.google.bigtable.admin.v2.Cluster.EncryptionConfig; import com.google.cloud.bigtable.admin.v2.internal.NameUtil; import org.junit.Test; import org.junit.runner.RunWith; @@ -151,4 +154,28 @@ public void testOptionalFields() { assertThat(actual).isEqualTo(expected); } + + @Test + public void testCmek() { + String kmsKeyName = + "projects/my-project/locations/us-east1-c/keyRings/my-key-ring/cryptoKeys/my-key"; + + CreateInstanceRequest input = + CreateInstanceRequest.of("my-instance") + .addCmekCluster("cluster1", "us-east1-c", 1, StorageType.SSD, kmsKeyName); + + com.google.bigtable.admin.v2.CreateInstanceRequest actual = input.toProto("my-project"); + + assertThat(actual) + .comparingExpectedFieldsOnly() + .isEqualTo( + com.google.bigtable.admin.v2.CreateInstanceRequest.newBuilder() + .putClusters( + "cluster1", + Cluster.newBuilder() + .setEncryptionConfig( + EncryptionConfig.newBuilder().setKmsKeyName(kmsKeyName).build()) + .build()) + .build()); + } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/EncryptionInfoTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/EncryptionInfoTest.java new file mode 100644 index 000000000..d0d077be3 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/EncryptionInfoTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.admin.v2.models; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.bigtable.admin.v2.EncryptionInfo.EncryptionType; +import com.google.cloud.bigtable.common.Status; +import com.google.common.base.Objects; +import com.google.rpc.Code; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class EncryptionInfoTest { + + @Test + public void testAllTypes() { + for (EncryptionType protoValue : + com.google.bigtable.admin.v2.EncryptionInfo.EncryptionType.values()) { + EncryptionInfo.Type modelValue = EncryptionInfo.Type.fromProto(protoValue); + + assertWithMessage("proto enum value %s should be wrapped", protoValue.toString()) + .that(modelValue.toString()) + .isEqualTo(protoValue.toString()); + } + + com.google.bigtable.admin.v2.EncryptionInfo randomEncryptionInfo = + com.google.bigtable.admin.v2.EncryptionInfo.newBuilder().setEncryptionTypeValue(14).build(); + assertWithMessage("Unrecognized proto enum value should be wrapped") + .that(EncryptionInfo.Type.fromProto(randomEncryptionInfo.getEncryptionType())) + .isEqualTo(EncryptionInfo.Type.UNRECOGNIZED); + } + + @Test + public void testFromProto() { + com.google.rpc.Status protoStatus = + com.google.rpc.Status.newBuilder() + .setCode(Code.UNAVAILABLE.getNumber()) + .setMessage("kms is unavailable") + .build(); + + com.google.bigtable.admin.v2.EncryptionInfo proto = + com.google.bigtable.admin.v2.EncryptionInfo.newBuilder() + .setEncryptionType(EncryptionType.CUSTOMER_MANAGED_ENCRYPTION) + .setKmsKeyVersion("some version") + .setEncryptionStatus(protoStatus) + .build(); + EncryptionInfo encryptionInfo = EncryptionInfo.fromProto(proto); + + assertThat(encryptionInfo.getStatus()).isEqualTo(Status.fromProto(protoStatus)); + assertThat(encryptionInfo.getType()).isEqualTo(EncryptionInfo.Type.CUSTOMER_MANAGED_ENCRYPTION); + assertThat(encryptionInfo.getKmsKeyVersion()).isEqualTo("some version"); + assertThat(encryptionInfo.toString()).isEqualTo(proto.toString()); + assertThat(encryptionInfo.hashCode()).isEqualTo(Objects.hashCode(proto)); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/common/StatusTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/common/StatusTest.java new file mode 100644 index 000000000..dccbd3440 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/common/StatusTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.common; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.rpc.Code; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class StatusTest { + + @Test + public void testAllCodes() { + for (Code protoValue : com.google.rpc.Code.values()) { + Status.Code modelValue = Status.Code.fromProto(protoValue); + + assertWithMessage("proto enum value %s should be wrapped", protoValue.toString()) + .that(modelValue.toString()) + .isEqualTo(protoValue.toString()); + } + + com.google.rpc.Status randomProto = + com.google.rpc.Status.newBuilder().setCode(49).setMessage("some message").build(); + assertWithMessage("Unrecognized proto value should be wrapped") + .that(Status.Code.fromProto(com.google.rpc.Code.forNumber(randomProto.getCode()))) + .isEqualTo(Status.Code.UNRECOGNIZED); + } + + @Test + public void testAllCodeNumbers() { + for (Code protoValue : com.google.rpc.Code.values()) { + if (protoValue == com.google.rpc.Code.UNRECOGNIZED) { + continue; + } + Status.Code modelValue = Status.Code.fromCodeNumber(protoValue.getNumber()); + + assertWithMessage("proto enum value %s should be wrapped", protoValue.toString()) + .that(modelValue.toString()) + .isEqualTo(protoValue.toString()); + } + + assertWithMessage("Unrecognized proto enum value should be wrapped") + .that(Status.Code.fromCodeNumber(-1)) + .isEqualTo(Status.Code.UNRECOGNIZED); + } + + @Test + public void testFromProto() { + com.google.rpc.Status proto = + com.google.rpc.Status.newBuilder() + .setCode(Code.UNAVAILABLE.getNumber()) + .setMessage("some message") + .build(); + + Status model = Status.fromProto(proto); + assertThat(model.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(model.getMessage()).isEqualTo("some message"); + } + + @Test + public void testToProto() { + com.google.rpc.Code code = Code.UNAVAILABLE; + com.google.rpc.Status proto = + com.google.rpc.Status.newBuilder() + .setCode(code.getNumber()) + .setMessage("some message") + .build(); + + Status model = Status.fromProto(proto); + assertThat(model.getCode().toProto()).isEqualTo(code); + assertThat(model.toProto()).isEqualTo(proto); + + assertThat(model.toString()).isEqualTo(proto.toString()); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/AbstractTestEnv.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/AbstractTestEnv.java index f183e5eea..4fcc53f7c 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/AbstractTestEnv.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/AbstractTestEnv.java @@ -16,7 +16,9 @@ package com.google.cloud.bigtable.test_helpers.env; import com.google.cloud.bigtable.admin.v2.BigtableInstanceAdminClient; +import com.google.cloud.bigtable.admin.v2.BigtableInstanceAdminSettings; import com.google.cloud.bigtable.admin.v2.BigtableTableAdminClient; +import com.google.cloud.bigtable.admin.v2.BigtableTableAdminSettings; 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; @@ -54,12 +56,18 @@ public abstract BigtableTableAdminClient getTableAdminClientForInstance(String i public abstract BigtableDataSettings getDataClientSettings(); + public abstract BigtableInstanceAdminSettings getInstanceAdminClientSettings(); + + public abstract BigtableTableAdminSettings getTableAdminSettings(); + public abstract String getProjectId(); public abstract String getInstanceId(); public abstract String getTableId(); + public abstract String getKmsKeyName(); + public String getFamilyId() { return "cf"; } @@ -88,10 +96,18 @@ public boolean isDirectPathIpv4Only() { return Boolean.getBoolean("bigtable.directpath-ipv4only"); } + public boolean shouldWaitForCmekKeyStatusUpdate() { + return Boolean.getBoolean("bigtable.wait-for-cmek-key-status"); + } + public String getPrimaryZone() { return "us-central1-b"; } + public String getPrimaryRegionSecondZone() { + return "us-central1-c"; + } + public String getSecondaryZone() { return "us-east1-b"; } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/CloudEnv.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/CloudEnv.java index 6afe733ab..1de8ee53b 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/CloudEnv.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/CloudEnv.java @@ -63,10 +63,12 @@ class CloudEnv extends AbstractTestEnv { private static final String PROJECT_PROPERTY_NAME = "bigtable.project"; private static final String INSTANCE_PROPERTY_NAME = "bigtable.instance"; private static final String TABLE_PROPERTY_NAME = "bigtable.table"; + private static final String CMEK_KMS_KEY_PROPERTY_NAME = "bigtable.kms_key_name"; private final String projectId; private final String instanceId; private final String tableId; + private final String kmsKeyName; private final BigtableDataSettings.Builder dataSettings; private final BigtableTableAdminSettings.Builder tableAdminSettings; @@ -80,6 +82,7 @@ static CloudEnv fromSystemProperties() { return new CloudEnv( getOptionalProperty(DATA_ENDPOINT_PROPERTY_NAME, ""), getOptionalProperty(ADMIN_ENDPOINT_PROPERTY_NAME, ""), + getOptionalProperty(CMEK_KMS_KEY_PROPERTY_NAME, ""), getRequiredProperty(PROJECT_PROPERTY_NAME), getRequiredProperty(INSTANCE_PROPERTY_NAME), getRequiredProperty(TABLE_PROPERTY_NAME)); @@ -88,12 +91,14 @@ static CloudEnv fromSystemProperties() { private CloudEnv( @Nullable String dataEndpoint, @Nullable String adminEndpoint, + @Nullable String kmsKeyName, String projectId, String instanceId, String tableId) { this.projectId = projectId; this.instanceId = instanceId; this.tableId = tableId; + this.kmsKeyName = kmsKeyName; this.dataSettings = BigtableDataSettings.newBuilder().setProjectId(projectId).setInstanceId(instanceId); @@ -191,6 +196,25 @@ public BigtableDataSettings getDataClientSettings() { return dataSettings.build(); } + @Override + public BigtableInstanceAdminSettings getInstanceAdminClientSettings() { + try { + return instanceAdminSettings.build(); + } catch (IOException e) { + throw new IllegalStateException( + "Caught unexpected error building instance admin settings", e); + } + } + + @Override + public BigtableTableAdminSettings getTableAdminSettings() { + try { + return tableAdminSettings.build(); + } catch (IOException e) { + throw new IllegalStateException("Caught unexpected error building table admin settings", e); + } + } + @Override public String getProjectId() { return projectId; @@ -206,6 +230,10 @@ public String getTableId() { return tableId; } + public String getKmsKeyName() { + return kmsKeyName; + } + private static String getOptionalProperty(String prop, String defaultValue) { return MoreObjects.firstNonNull(System.getProperty(prop), defaultValue); } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/EmulatorEnv.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/EmulatorEnv.java index edb9439dc..e4b0a2651 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/EmulatorEnv.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/EmulatorEnv.java @@ -16,6 +16,7 @@ package com.google.cloud.bigtable.test_helpers.env; import com.google.cloud.bigtable.admin.v2.BigtableInstanceAdminClient; +import com.google.cloud.bigtable.admin.v2.BigtableInstanceAdminSettings; import com.google.cloud.bigtable.admin.v2.BigtableTableAdminClient; import com.google.cloud.bigtable.admin.v2.BigtableTableAdminSettings; import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; @@ -38,6 +39,7 @@ public class EmulatorEnv extends AbstractTestEnv { private BigtableDataClient dataClient; private BigtableDataSettings dataSettings; + private BigtableTableAdminSettings tableAdminSettings; public static EmulatorEnv createBundled() { return new EmulatorEnv(); @@ -63,12 +65,13 @@ void start() throws Exception { dataClient = BigtableDataClient.create(dataSettings); - tableAdminClient = - BigtableTableAdminClient.create( - BigtableTableAdminSettings.newBuilderForEmulator(emulator.getPort()) - .setProjectId("fake-project") - .setInstanceId("fake-instance") - .build()); + tableAdminSettings = + BigtableTableAdminSettings.newBuilderForEmulator(emulator.getPort()) + .setProjectId("fake-project") + .setInstanceId("fake-instance") + .build(); + + tableAdminClient = BigtableTableAdminClient.create(tableAdminSettings); tableAdminClient.createTable(CreateTableRequest.of(TABLE_ID).addFamily(getFamilyId())); } @@ -85,6 +88,16 @@ public BigtableDataSettings getDataClientSettings() { return dataSettings; } + @Override + public BigtableInstanceAdminSettings getInstanceAdminClientSettings() { + throw new UnsupportedOperationException("instance admin is not support by the emulator"); + } + + @Override + public BigtableTableAdminSettings getTableAdminSettings() { + return tableAdminSettings; + } + @Override public String getProjectId() { return PROJECT_ID; @@ -126,6 +139,10 @@ public BigtableInstanceAdminClient getInstanceAdminClient() { throw new UnsupportedOperationException("InstanceAdminClient is not supported with emulator"); } + public String getKmsKeyName() { + throw new UnsupportedOperationException("CMEK is not supported with emulator"); + } + @Override public boolean isInstanceAdminSupported() { return false; diff --git a/pom.xml b/pom.xml index 8428244c5..59f778a44 100644 --- a/pom.xml +++ b/pom.xml @@ -183,6 +183,12 @@ truth 1.1.2 + + com.google.truth.extensions + truth-proto-extension + 1.0.1 + test + junit junit