diff --git a/bigtable/deployment/pom.xml b/bigtable/deployment/pom.xml
index 6a13616f..e7922f8a 100644
--- a/bigtable/deployment/pom.xml
+++ b/bigtable/deployment/pom.xml
@@ -27,6 +27,10 @@
io.quarkiverse.googlecloudservices
quarkus-google-cloud-bigtable
+
+ org.testcontainers
+ gcloud
+
@@ -46,4 +50,4 @@
-
\ No newline at end of file
+
diff --git a/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableBuildSteps.java b/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableBuildSteps.java
index e6f42b5d..edafc56c 100644
--- a/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableBuildSteps.java
+++ b/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableBuildSteps.java
@@ -4,7 +4,7 @@
import io.quarkus.deployment.builditem.FeatureBuildItem;
public class BigtableBuildSteps {
- private static final String FEATURE = "google-cloud-bigtable";
+ protected static final String FEATURE = "google-cloud-bigtable";
@BuildStep
public FeatureBuildItem feature() {
diff --git a/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableBuildTimeConfig.java b/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableBuildTimeConfig.java
new file mode 100644
index 00000000..91b39bbe
--- /dev/null
+++ b/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableBuildTimeConfig.java
@@ -0,0 +1,22 @@
+package io.quarkiverse.googlecloudservices.bigtable.deployment;
+
+import io.quarkus.runtime.annotations.ConfigItem;
+import io.quarkus.runtime.annotations.ConfigPhase;
+import io.quarkus.runtime.annotations.ConfigRoot;
+
+/**
+ * Root configuration class for Bigtable that operates at build time.
+ * This class provides a nested structure for configuration, including
+ * a separate group for the development service configuration.
+ */
+@ConfigRoot(name = "google.cloud.bigtable", phase = ConfigPhase.BUILD_TIME)
+public class BigtableBuildTimeConfig {
+
+ /**
+ * Configuration for the Bigtable dev service.
+ * These settings will be used when Bigtable service is being configured
+ * for development purposes.
+ */
+ @ConfigItem
+ public BigtableDevServiceConfig devservice;
+}
diff --git a/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableDevServiceConfig.java b/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableDevServiceConfig.java
new file mode 100644
index 00000000..1a5c8b2d
--- /dev/null
+++ b/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableDevServiceConfig.java
@@ -0,0 +1,44 @@
+package io.quarkiverse.googlecloudservices.bigtable.deployment;
+
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+/**
+ * Configuration group for the Bigtable dev service. This class holds all the configuration properties
+ * related to the Google Cloud Bigtable service for development environments.
+ *
+ * Here is an example of how to configure these properties:
+ *
+ *
+ *
+ * quarkus.google.cloud.bigtable.deservice.enabled = true
+ * quarkus.google.cloud.bigtable.deservice.image-name = gcr.io/google.com/cloudsdktool/google-cloud-cli # optional
+ * quarkus.google.cloud.bigtable.deservice.emulatorPort = 9000 # optional
+ *
+ */
+@ConfigGroup
+public class BigtableDevServiceConfig {
+
+ /**
+ * Indicates whether the Bigtable service should be enabled or not.
+ * The default value is 'false'.
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean enabled;
+
+ /**
+ * Sets the Docker image name for the Google Cloud SDK.
+ * This image is used to emulate the Bigtable service in the development environment.
+ * The default value is 'gcr.io/google.com/cloudsdktool/google-cloud-cli'.
+ */
+ @ConfigItem(defaultValue = "gcr.io/google.com/cloudsdktool/google-cloud-cli")
+ public String imageName;
+
+ /**
+ * Specifies the emulatorPort on which the Bigtable service should run in the development environment.
+ */
+ @ConfigItem
+ public Optional emulatorPort = Optional.empty();
+}
diff --git a/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableDevServiceProcessor.java b/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableDevServiceProcessor.java
new file mode 100644
index 00000000..bc33b207
--- /dev/null
+++ b/bigtable/deployment/src/main/java/io/quarkiverse/googlecloudservices/bigtable/deployment/BigtableDevServiceProcessor.java
@@ -0,0 +1,176 @@
+package io.quarkiverse.googlecloudservices.bigtable.deployment;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+
+import org.jboss.logging.Logger;
+import org.testcontainers.containers.BigtableEmulatorContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import io.quarkus.deployment.IsNormal;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.BuildSteps;
+import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem;
+import io.quarkus.deployment.builditem.DevServicesResultBuildItem;
+import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem;
+import io.quarkus.deployment.builditem.DockerStatusBuildItem;
+import io.quarkus.deployment.builditem.LaunchModeBuildItem;
+import io.quarkus.deployment.console.ConsoleInstalledBuildItem;
+import io.quarkus.deployment.console.StartupLogCompressor;
+import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
+import io.quarkus.deployment.logging.LoggingSetupBuildItem;
+
+/**
+ * Processor responsible for managing Bigtable Dev Services.
+ *
+ * The processor starts the Bigtable service in case it's not running.
+ */
+@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class)
+public class BigtableDevServiceProcessor {
+
+ private static final Logger LOGGER = Logger.getLogger(BigtableDevServiceProcessor.class.getName());
+
+ // Running dev service instance
+ private static volatile DevServicesResultBuildItem.RunningDevService devService;
+ // Configuration for the Bigtable Dev service
+ private static volatile BigtableDevServiceConfig config;
+
+ @BuildStep
+ public DevServicesResultBuildItem start(DockerStatusBuildItem dockerStatusBuildItem,
+ BigtableBuildTimeConfig buildTimeConfig,
+ List devServicesSharedNetworkBuildItem,
+ Optional consoleInstalledBuildItem,
+ CuratedApplicationShutdownBuildItem closeBuildItem,
+ LaunchModeBuildItem launchMode,
+ LoggingSetupBuildItem loggingSetupBuildItem,
+ GlobalDevServicesConfig globalDevServicesConfig) {
+ // If dev service is running and config has changed, stop the service
+ if (devService != null && !buildTimeConfig.devservice.equals(config)) {
+ stopContainer();
+ } else if (devService != null) {
+ return devService.toBuildItem();
+ }
+
+ // Set up log compressor for startup logs
+ StartupLogCompressor compressor = new StartupLogCompressor(
+ (launchMode.isTest() ? "(test) " : "") + "Google Cloud Bigtable Dev Services Starting:",
+ consoleInstalledBuildItem,
+ loggingSetupBuildItem);
+
+ // Try starting the container if conditions are met
+ try {
+ devService = startContainerIfAvailable(dockerStatusBuildItem, buildTimeConfig.devservice,
+ globalDevServicesConfig.timeout);
+ } catch (Throwable t) {
+ LOGGER.warn("Unable to start Bigtable dev service", t);
+ // Dump captured logs in case of an error
+ compressor.closeAndDumpCaptured();
+ return null;
+ } finally {
+ compressor.close();
+ }
+
+ return devService == null ? null : devService.toBuildItem();
+ }
+
+ /**
+ * Start the container if conditions are met.
+ *
+ * @param dockerStatusBuildItem, Docker status
+ * @param config, Configuration for the Bigtable service
+ * @param timeout, Optional timeout for starting the service
+ * @return Running service item, or null if the service couldn't be started
+ */
+ private DevServicesResultBuildItem.RunningDevService startContainerIfAvailable(DockerStatusBuildItem dockerStatusBuildItem,
+ BigtableDevServiceConfig config,
+ Optional timeout) {
+ if (!config.enabled) {
+ // Bigtable service explicitly disabled
+ LOGGER.debug("Not starting Dev Services for Bigtable as it has been disabled in the config");
+ return null;
+ }
+
+ if (!dockerStatusBuildItem.isDockerAvailable()) {
+ LOGGER.warn("Not starting devservice because docker is not available");
+ return null;
+ }
+
+ return startContainer(dockerStatusBuildItem, config, timeout);
+ }
+
+ /**
+ * Starts the Bigtable emulator container with provided configuration.
+ *
+ * @param dockerStatusBuildItem, Docker status
+ * @param config, Configuration for the Bigtable service
+ * @param timeout, Optional timeout for starting the service
+ * @return Running service item, or null if the service couldn't be started
+ */
+ private DevServicesResultBuildItem.RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuildItem,
+ BigtableDevServiceConfig config,
+ Optional timeout) {
+ // Create and configure Bigtable emulator container
+ BigtableEmulatorContainer emulatorContainer = new QuarkusBigtableContainer(
+ DockerImageName.parse(config.imageName).asCompatibleSubstituteFor("gcr.io/google.com/cloudsdktool/cloud-sdk"),
+ config.emulatorPort.orElse(null));
+
+ // Set container startup timeout if provided
+ timeout.ifPresent(emulatorContainer::withStartupTimeout);
+ emulatorContainer.start();
+
+ // Set the config for the started container
+ BigtableDevServiceProcessor.config = config;
+
+ // Return running service item with container details
+ return new DevServicesResultBuildItem.RunningDevService(BigtableBuildSteps.FEATURE,
+ emulatorContainer.getContainerId(),
+ emulatorContainer::close, "quarkus.google.cloud.bigtable.emulator-host",
+ emulatorContainer.getEmulatorEndpoint());
+ }
+
+ /**
+ * Stops the running Bigtable emulator container.
+ */
+ private void stopContainer() {
+ if (devService != null && devService.isOwner()) {
+ try {
+ // Try closing the running dev service
+ devService.close();
+ } catch (Throwable e) {
+ LOGGER.error("Failed to stop Bigtable container", e);
+ } finally {
+ devService = null;
+ }
+ }
+ }
+
+ /**
+ * Class for creating and configuring a Bigtable emulator container.
+ */
+ private static class QuarkusBigtableContainer extends BigtableEmulatorContainer {
+
+ private final Integer fixedExposedPort;
+ private static final int INTERNAL_PORT = 9000;
+
+ private QuarkusBigtableContainer(DockerImageName dockerImageName, Integer fixedExposedPort) {
+ super(dockerImageName);
+ this.fixedExposedPort = fixedExposedPort;
+ }
+
+ /**
+ * Configures the Bigtable emulator container.
+ */
+ @Override
+ public void configure() {
+ super.configure();
+
+ // Expose Bigtable emulatorPort
+ if (fixedExposedPort != null) {
+ addFixedExposedPort(fixedExposedPort, INTERNAL_PORT);
+ } else {
+ addExposedPort(INTERNAL_PORT);
+ }
+ }
+ }
+}
diff --git a/integration-tests/main/src/main/java/io/quarkiverse/googlecloudservices/it/BigtableResource.java b/integration-tests/main/src/main/java/io/quarkiverse/googlecloudservices/it/BigtableResource.java
index 363631a0..4fef8997 100644
--- a/integration-tests/main/src/main/java/io/quarkiverse/googlecloudservices/it/BigtableResource.java
+++ b/integration-tests/main/src/main/java/io/quarkiverse/googlecloudservices/it/BigtableResource.java
@@ -10,15 +10,24 @@
import org.eclipse.microprofile.config.inject.ConfigProperty;
import com.google.api.gax.core.CredentialsProvider;
+import com.google.api.gax.core.NoCredentialsProvider;
+import com.google.api.gax.grpc.GrpcTransportChannel;
+import com.google.api.gax.rpc.FixedTransportChannelProvider;
+import com.google.api.gax.rpc.TransportChannelProvider;
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;
+import com.google.cloud.bigtable.admin.v2.stub.BigtableTableAdminStubSettings;
+import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub;
import com.google.cloud.bigtable.data.v2.BigtableDataClient;
import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
import com.google.cloud.bigtable.data.v2.models.Row;
import com.google.cloud.bigtable.data.v2.models.RowCell;
import com.google.cloud.bigtable.data.v2.models.RowMutation;
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+
@Path("/bigtable")
public class BigtableResource {
private static final String INSTANCE_ID = "test-instance";
@@ -31,21 +40,47 @@ public class BigtableResource {
@ConfigProperty(name = "bigtable.authenticated", defaultValue = "true")
boolean authenticated;
+ @ConfigProperty(name = "quarkus.google.cloud.bigtable.emulator-host")
+ String emulatorHost;
+
@Inject
CredentialsProvider credentialsProvider;
@PostConstruct
void initBigtable() throws IOException {
- BigtableTableAdminSettings.Builder settings = BigtableTableAdminSettings.newBuilder()
- .setProjectId(projectId)
- .setInstanceId(INSTANCE_ID);
- if (authenticated) {
- settings.setCredentialsProvider(credentialsProvider);
- }
- try (BigtableTableAdminClient adminClient = BigtableTableAdminClient.create(settings.build())) {
- if (!adminClient.exists(TABLE_ID)) {
- CreateTableRequest createTableRequest = CreateTableRequest.of(TABLE_ID).addFamily(COLUMN_FAMILY_ID);
- adminClient.createTable(createTableRequest);
+ if (emulatorHost != null) {
+ ManagedChannel channel = ManagedChannelBuilder.forTarget(emulatorHost).usePlaintext().build();
+
+ TransportChannelProvider channelProvider = FixedTransportChannelProvider.create(
+ GrpcTransportChannel.create(channel));
+ NoCredentialsProvider credentialsProvider = NoCredentialsProvider.create();
+
+ EnhancedBigtableTableAdminStub stub = EnhancedBigtableTableAdminStub.createEnhanced(
+ BigtableTableAdminStubSettings
+ .newBuilder()
+ .setTransportChannelProvider(channelProvider)
+ .setCredentialsProvider(credentialsProvider)
+ .build());
+
+ try (BigtableTableAdminClient adminClient = BigtableTableAdminClient.create(projectId, INSTANCE_ID, stub)) {
+ if (!adminClient.exists(TABLE_ID)) {
+ CreateTableRequest createTableRequest = CreateTableRequest.of(TABLE_ID).addFamily(COLUMN_FAMILY_ID);
+ adminClient.createTable(createTableRequest);
+ }
+ }
+ } else {
+ BigtableTableAdminSettings.Builder settings = BigtableTableAdminSettings.newBuilder()
+ .setProjectId(projectId)
+ .setInstanceId(INSTANCE_ID);
+
+ if (authenticated) {
+ settings.setCredentialsProvider(credentialsProvider);
+ }
+ try (BigtableTableAdminClient adminClient = BigtableTableAdminClient.create(settings.build())) {
+ if (!adminClient.exists(TABLE_ID)) {
+ CreateTableRequest createTableRequest = CreateTableRequest.of(TABLE_ID).addFamily(COLUMN_FAMILY_ID);
+ adminClient.createTable(createTableRequest);
+ }
}
}
}
@@ -55,6 +90,17 @@ public String bigtable() throws IOException {
BigtableDataSettings.Builder settings = BigtableDataSettings.newBuilder()
.setProjectId(projectId)
.setInstanceId(INSTANCE_ID);
+
+ if (emulatorHost != null) {
+ String[] hostAndPort = emulatorHost.split(":");
+ String host = hostAndPort[0];
+ int port = Integer.parseInt(hostAndPort[1]);
+
+ settings = BigtableDataSettings.newBuilderForEmulator(host, port)
+ .setProjectId(projectId)
+ .setInstanceId(INSTANCE_ID);
+ }
+
if (authenticated) {
settings.setCredentialsProvider(credentialsProvider);
}
diff --git a/integration-tests/main/src/main/resources/application.properties b/integration-tests/main/src/main/resources/application.properties
index a2d1e85f..0db61b30 100644
--- a/integration-tests/main/src/main/resources/application.properties
+++ b/integration-tests/main/src/main/resources/application.properties
@@ -14,6 +14,7 @@
%test.pubsub.use-emulator=true
%test.quarkus.google.cloud.pubsub.devservice.enabled=true
%test.quarkus.google.cloud.firestore.devservice.enabled=true
+%test.quarkus.google.cloud.bigtable.devservice.enabled=true
# Secret Manager Demo
# You can load secrets from Google Cloud Secret Manager with the ${sm//} syntax.
diff --git a/integration-tests/main/src/test/java/io/quarkiverse/googlecloudservices/it/BigtableResourceTest.java b/integration-tests/main/src/test/java/io/quarkiverse/googlecloudservices/it/BigtableResourceTest.java
index 397dc31f..76dccfe4 100644
--- a/integration-tests/main/src/test/java/io/quarkiverse/googlecloudservices/it/BigtableResourceTest.java
+++ b/integration-tests/main/src/test/java/io/quarkiverse/googlecloudservices/it/BigtableResourceTest.java
@@ -2,38 +2,14 @@
import static io.restassured.RestAssured.given;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
-import org.junitpioneer.jupiter.SetEnvironmentVariable;
-import org.testcontainers.containers.BigtableEmulatorContainer;
-import org.testcontainers.utility.DockerImageName;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
public class BigtableResourceTest {
- private static final BigtableEmulatorContainer EMULATOR = new BigtableEmulatorContainer(
- DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk"));
-
- @BeforeAll
- public static void startGcloudContainer() {
- List portBindings = new ArrayList<>();
- portBindings.add("9000:9000");
- EMULATOR.setPortBindings(portBindings);
- EMULATOR.start();
- }
-
- @AfterAll
- public static void stopGcloudContainer() {
- EMULATOR.stop();
- }
@Test
- @SetEnvironmentVariable(key = "BIGTABLE_EMULATOR_HOST", value = "localhost:9000")
public void testBigtable() {
given()
.when().get("/bigtable")