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")