diff --git a/docs/modules/gcloud.md b/docs/modules/gcloud.md
new file mode 100644
index 00000000000..3400ad881a9
--- /dev/null
+++ b/docs/modules/gcloud.md
@@ -0,0 +1,44 @@
+# GCloud Module
+
+!!! note
+ This module is INCUBATING. While it is ready for use and operational in the current version of Testcontainers, it is possible that it may receive breaking changes in the future. See [our contributing guidelines](/contributing/#incubating-modules) for more information on our incubating modules policy.
+
+Testcontainers module for the Google's [Cloud SDK](https://cloud.google.com/sdk/).
+
+Currently, the module supports `Datastore`, `Firestore`, `Pub/Sub` and `Spanner` emulators. In order to use it, you should use the following classes:
+
+* DatastoreEmulatorContainer
+* FirestoreEmulatorContainer
+* PubSubEmulatorContainer
+* SpannerEmulatorContainer
+
+## Usage example
+
+Running GCloud as a stand-in for Google Datastore during a test:
+
+
+[Creating a Datastore container](../../modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java) inside_block:creatingDatastoreEmulatorContainer
+
+
+And how to start it:
+
+
+[Starting a Datastore container](../../modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java) inside_block:startingDatastoreEmulatorContainer
+
+
+## Adding this module to your project dependencies
+
+Add the following dependency to your `pom.xml`/`build.gradle` file:
+
+```groovy tab='Gradle'
+testCompile "org.testcontainers:gcloud:{{latest_version}}"
+```
+
+```xml tab='Maven'
+
+ org.testcontainers
+ gcloud
+ {{latest_version}}
+ test
+
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index b8372e81ed8..dfff99bb302 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -60,6 +60,7 @@ nav:
- modules/databases/presto.md
- modules/docker_compose.md
- modules/elasticsearch.md
+ - modules/gcloud.md
- modules/kafka.md
- modules/localstack.md
- modules/mockserver.md
diff --git a/modules/gcloud/build.gradle b/modules/gcloud/build.gradle
new file mode 100644
index 00000000000..a5737648a4b
--- /dev/null
+++ b/modules/gcloud/build.gradle
@@ -0,0 +1,11 @@
+description = "Testcontainers :: GCloud"
+
+dependencies {
+ compile project(':testcontainers')
+
+ testCompile 'com.google.cloud:google-cloud-datastore:1.102.4'
+ testCompile 'com.google.cloud:google-cloud-firestore:1.33.0'
+ testCompile 'com.google.cloud:google-cloud-pubsub:1.105.0'
+ testCompile 'com.google.cloud:google-cloud-spanner:1.50.0'
+ testCompile 'org.assertj:assertj-core:3.15.0'
+}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/DatastoreEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/DatastoreEmulatorContainer.java
new file mode 100644
index 00000000000..85b07b94591
--- /dev/null
+++ b/modules/gcloud/src/main/java/org/testcontainers/containers/DatastoreEmulatorContainer.java
@@ -0,0 +1,29 @@
+package org.testcontainers.containers;
+
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A Datastore container that relies in google cloud sdk.
+ *
+ * Default port is 8081.
+ *
+ * @author Eddú Meléndez
+ */
+public class DatastoreEmulatorContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk");
+
+ private static final String CMD = "gcloud beta emulators datastore start --project test-project --host-port 0.0.0.0:8081";
+
+ public DatastoreEmulatorContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+
+ withExposedPorts(8081);
+ setWaitStrategy(Wait.forHttp("/").forStatusCode(200));
+ withCommand("/bin/sh", "-c", CMD);
+ }
+
+}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java
new file mode 100644
index 00000000000..41442f0f531
--- /dev/null
+++ b/modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java
@@ -0,0 +1,30 @@
+package org.testcontainers.containers;
+
+import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A Firestore container that relies in google cloud sdk.
+ *
+ * Default port is 8080.
+ *
+ * @author Eddú Meléndez
+ */
+public class FirestoreEmulatorContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk");
+
+ private static final String CMD = "gcloud beta emulators firestore start --host-port 0.0.0.0:8080";
+
+ public FirestoreEmulatorContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+
+ withExposedPorts(8080);
+ setWaitStrategy(new LogMessageWaitStrategy()
+ .withRegEx("(?s).*running.*$"));
+ withCommand("/bin/sh", "-c", CMD);
+ }
+
+}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/PubSubEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/PubSubEmulatorContainer.java
new file mode 100644
index 00000000000..c92145b3d3a
--- /dev/null
+++ b/modules/gcloud/src/main/java/org/testcontainers/containers/PubSubEmulatorContainer.java
@@ -0,0 +1,30 @@
+package org.testcontainers.containers;
+
+import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A PubSub container that relies in google cloud sdk.
+ *
+ * Default port is 8085.
+ *
+ * @author Eddú Meléndez
+ */
+public class PubSubEmulatorContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk");
+
+ private static final String CMD = "gcloud beta emulators pubsub start --host-port 0.0.0.0:8085";
+
+ public PubSubEmulatorContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+
+ withExposedPorts(8085);
+ setWaitStrategy(new LogMessageWaitStrategy()
+ .withRegEx("(?s).*started.*$"));
+ withCommand("/bin/sh", "-c", CMD);
+ }
+
+}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/SpannerEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/SpannerEmulatorContainer.java
new file mode 100644
index 00000000000..5f2a203d911
--- /dev/null
+++ b/modules/gcloud/src/main/java/org/testcontainers/containers/SpannerEmulatorContainer.java
@@ -0,0 +1,28 @@
+package org.testcontainers.containers;
+
+import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A Spanner container. Default ports: 9010 for GRPC and 9020 for HTTP.
+ *
+ * @author Eddú Meléndez
+ */
+public class SpannerEmulatorContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("gcr.io/cloud-spanner-emulator/emulator");
+
+ private static final int GRPC_PORT = 9010;
+ private static final int HTTP_PORT = 9020;
+
+ public SpannerEmulatorContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+
+ withExposedPorts(GRPC_PORT, HTTP_PORT);
+ setWaitStrategy(new LogMessageWaitStrategy()
+ .withRegEx(".*Cloud Spanner emulator running\\..*"));
+ }
+
+}
diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java
new file mode 100644
index 00000000000..f8285e4c691
--- /dev/null
+++ b/modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java
@@ -0,0 +1,43 @@
+package org.testcontainers.containers;
+
+import com.google.cloud.NoCredentials;
+import com.google.cloud.ServiceOptions;
+import com.google.cloud.datastore.Datastore;
+import com.google.cloud.datastore.DatastoreOptions;
+import com.google.cloud.datastore.Entity;
+import com.google.cloud.datastore.Key;
+import org.junit.Rule;
+import org.junit.Test;
+import org.testcontainers.utility.DockerImageName;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DatastoreEmulatorContainerTest {
+
+ @Rule
+ // creatingDatastoreEmulatorContainer {
+ public DatastoreEmulatorContainer emulator = new DatastoreEmulatorContainer(
+ DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk:313.0.0")
+ );
+ // }
+
+ // startingDatastoreEmulatorContainer {
+ @Test
+ public void testSimple() {
+ DatastoreOptions options = DatastoreOptions.newBuilder()
+ .setHost(emulator.getContainerIpAddress() + ":" + emulator.getMappedPort(8081))
+ .setCredentials(NoCredentials.getInstance())
+ .setRetrySettings(ServiceOptions.getNoRetrySettings())
+ .setProjectId("test-project")
+ .build();
+ Datastore datastore = options.getService();
+
+ Key key = datastore.newKeyFactory().setKind("Task").newKey("sample");
+ Entity entity = Entity.newBuilder(key).set("description", "my description").build();
+ datastore.put(entity);
+
+ assertThat(datastore.get(key).getString("description")).isEqualTo("my description");
+ }
+ // }
+
+}
diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/FirestoreEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/containers/FirestoreEmulatorContainerTest.java
new file mode 100644
index 00000000000..2f23ff40483
--- /dev/null
+++ b/modules/gcloud/src/test/java/org/testcontainers/containers/FirestoreEmulatorContainerTest.java
@@ -0,0 +1,49 @@
+package org.testcontainers.containers;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import com.google.api.core.ApiFuture;
+import com.google.cloud.NoCredentials;
+import com.google.cloud.firestore.CollectionReference;
+import com.google.cloud.firestore.DocumentReference;
+import com.google.cloud.firestore.Firestore;
+import com.google.cloud.firestore.FirestoreOptions;
+import com.google.cloud.firestore.QuerySnapshot;
+import com.google.cloud.firestore.WriteResult;
+import org.junit.Rule;
+import org.junit.Test;
+import org.testcontainers.utility.DockerImageName;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class FirestoreEmulatorContainerTest {
+
+ @Rule
+ public FirestoreEmulatorContainer emulator = new FirestoreEmulatorContainer(DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk:313.0.0"));
+
+ @Test
+ public void testSimple() throws ExecutionException, InterruptedException {
+ FirestoreOptions options = FirestoreOptions.getDefaultInstance().toBuilder()
+ .setHost(emulator.getContainerIpAddress() + ":" + emulator.getMappedPort(8080))
+ .setCredentials(NoCredentials.getInstance())
+ .setProjectId("test-project")
+ .build();
+ Firestore firestore = options.getService();
+
+ CollectionReference users = firestore.collection("users");
+ DocumentReference docRef = users.document("alovelace");
+ Map data = new HashMap<>();
+ data.put("first", "Ada");
+ data.put("last", "Lovelace");
+ ApiFuture result = docRef.set(data);
+ result.get();
+
+ ApiFuture query = users.get();
+ QuerySnapshot querySnapshot = query.get();
+
+ assertThat(querySnapshot.getDocuments().get(0).getData()).containsEntry("first", "Ada");
+ }
+
+}
diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/PubSubEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/containers/PubSubEmulatorContainerTest.java
new file mode 100644
index 00000000000..0c5ec888de5
--- /dev/null
+++ b/modules/gcloud/src/test/java/org/testcontainers/containers/PubSubEmulatorContainerTest.java
@@ -0,0 +1,102 @@
+package org.testcontainers.containers;
+
+import java.io.IOException;
+
+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.pubsub.v1.Publisher;
+import com.google.cloud.pubsub.v1.SubscriptionAdminClient;
+import com.google.cloud.pubsub.v1.SubscriptionAdminSettings;
+import com.google.cloud.pubsub.v1.TopicAdminClient;
+import com.google.cloud.pubsub.v1.TopicAdminSettings;
+import com.google.cloud.pubsub.v1.stub.GrpcSubscriberStub;
+import com.google.cloud.pubsub.v1.stub.SubscriberStub;
+import com.google.cloud.pubsub.v1.stub.SubscriberStubSettings;
+import com.google.protobuf.ByteString;
+import com.google.pubsub.v1.ProjectSubscriptionName;
+import com.google.pubsub.v1.PubsubMessage;
+import com.google.pubsub.v1.PullRequest;
+import com.google.pubsub.v1.PullResponse;
+import com.google.pubsub.v1.PushConfig;
+import com.google.pubsub.v1.TopicName;
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import org.junit.Rule;
+import org.junit.Test;
+import org.testcontainers.utility.DockerImageName;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PubSubEmulatorContainerTest {
+
+ public static final String PROJECT_ID = "my-project-id";
+
+ @Rule
+ public PubSubEmulatorContainer emulator = new PubSubEmulatorContainer(DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk:313.0.0"));
+
+ @Test
+ public void testSimple() throws IOException {
+ String hostport = emulator.getContainerIpAddress() + ":" + emulator.getMappedPort(8085);
+ ManagedChannel channel = ManagedChannelBuilder.forTarget(hostport).usePlaintext().build();
+ try {
+ TransportChannelProvider channelProvider =
+ FixedTransportChannelProvider.create(GrpcTransportChannel.create(channel));
+ NoCredentialsProvider credentialsProvider = NoCredentialsProvider.create();
+
+ String topicId = "my-topic-id";
+ createTopic(topicId, channelProvider, credentialsProvider);
+
+ String subscriptionId = "my-subscription-id";
+ createSubscription(subscriptionId, topicId, channelProvider, credentialsProvider);
+
+ Publisher publisher = Publisher.newBuilder(TopicName.of(PROJECT_ID, topicId))
+ .setChannelProvider(channelProvider)
+ .setCredentialsProvider(credentialsProvider)
+ .build();
+ PubsubMessage message = PubsubMessage.newBuilder().setData(ByteString.copyFromUtf8("test message")).build();
+ publisher.publish(message);
+
+ SubscriberStubSettings subscriberStubSettings =
+ SubscriberStubSettings.newBuilder()
+ .setTransportChannelProvider(channelProvider)
+ .setCredentialsProvider(credentialsProvider)
+ .build();
+ try (SubscriberStub subscriber = GrpcSubscriberStub.create(subscriberStubSettings)) {
+ PullRequest pullRequest = PullRequest.newBuilder()
+ .setMaxMessages(1)
+ .setSubscription(ProjectSubscriptionName.format(PROJECT_ID, subscriptionId))
+ .build();
+ PullResponse pullResponse = subscriber.pullCallable().call(pullRequest);
+
+ assertThat(pullResponse.getReceivedMessagesList()).hasSize(1);
+ assertThat(pullResponse.getReceivedMessages(0).getMessage().getData().toStringUtf8()).isEqualTo("test message");
+ }
+ } finally {
+ channel.shutdown();
+ }
+ }
+
+ private void createTopic(String topicId, TransportChannelProvider channelProvider, NoCredentialsProvider credentialsProvider) throws IOException {
+ TopicAdminSettings topicAdminSettings = TopicAdminSettings.newBuilder()
+ .setTransportChannelProvider(channelProvider)
+ .setCredentialsProvider(credentialsProvider)
+ .build();
+ try (TopicAdminClient topicAdminClient = TopicAdminClient.create(topicAdminSettings)) {
+ TopicName topicName = TopicName.of(PROJECT_ID, topicId);
+ topicAdminClient.createTopic(topicName);
+ }
+ }
+
+ private void createSubscription(String subscriptionId, String topicId, TransportChannelProvider channelProvider, NoCredentialsProvider credentialsProvider) throws IOException {
+ SubscriptionAdminSettings subscriptionAdminSettings = SubscriptionAdminSettings.newBuilder()
+ .setTransportChannelProvider(channelProvider)
+ .setCredentialsProvider(credentialsProvider)
+ .build();
+ SubscriptionAdminClient subscriptionAdminClient = SubscriptionAdminClient.create(subscriptionAdminSettings);
+ ProjectSubscriptionName subscriptionName = ProjectSubscriptionName.of(PROJECT_ID, subscriptionId);
+ subscriptionAdminClient.createSubscription(subscriptionName, TopicName.of(PROJECT_ID, topicId), PushConfig.getDefaultInstance(), 10);
+ }
+
+}
diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/SpannerEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/containers/SpannerEmulatorContainerTest.java
new file mode 100644
index 00000000000..9093530236a
--- /dev/null
+++ b/modules/gcloud/src/test/java/org/testcontainers/containers/SpannerEmulatorContainerTest.java
@@ -0,0 +1,80 @@
+package org.testcontainers.containers;
+
+import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
+
+import com.google.cloud.NoCredentials;
+import com.google.cloud.spanner.Database;
+import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Instance;
+import com.google.cloud.spanner.InstanceAdminClient;
+import com.google.cloud.spanner.InstanceConfigId;
+import com.google.cloud.spanner.InstanceId;
+import com.google.cloud.spanner.InstanceInfo;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Spanner;
+import com.google.cloud.spanner.SpannerOptions;
+import com.google.cloud.spanner.Statement;
+import org.junit.Rule;
+import org.junit.Test;
+import org.testcontainers.utility.DockerImageName;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SpannerEmulatorContainerTest {
+
+ @Rule
+ public SpannerEmulatorContainer emulator = new SpannerEmulatorContainer(DockerImageName.parse("gcr.io/cloud-spanner-emulator/emulator:1.1.0"));
+
+ private static final String PROJECT_NAME = "test-project";
+ private static final String INSTANCE_NAME = "test-instance";
+ private static final String DATABASE_NAME = "test-database";
+
+ @Test
+ public void testSimple() throws ExecutionException, InterruptedException {
+ SpannerOptions options = SpannerOptions.newBuilder()
+ .setEmulatorHost(emulator.getContainerIpAddress() + ":" + emulator.getMappedPort(9010))
+ .setCredentials(NoCredentials.getInstance())
+ .setProjectId(PROJECT_NAME)
+ .build();
+
+ Spanner spanner = options.getService();
+
+ InstanceId instanceId = createInstance(spanner);
+
+ createDatabase(spanner);
+
+ DatabaseId databaseId = DatabaseId.of(instanceId, DATABASE_NAME);
+ DatabaseClient dbClient = spanner.getDatabaseClient(databaseId);
+ dbClient.readWriteTransaction()
+ .run(tx -> {
+ String sql1 = "Delete from TestTable where 1=1";
+ tx.executeUpdate(Statement.of(sql1));
+ String sql = "INSERT INTO TestTable (Key, Value) VALUES (1, 'Java'), (2, 'Go')";
+ tx.executeUpdate(Statement.of(sql));
+ return null;
+ });
+
+ ResultSet resultSet = dbClient.readOnlyTransaction()
+ .executeQuery(Statement.of("select * from TestTable order by Key"));
+ resultSet.next();
+ assertThat(resultSet.getLong(0)).isEqualTo(1);
+ assertThat(resultSet.getString(1)).isEqualTo("Java");
+ }
+
+ private void createDatabase(Spanner spanner) throws InterruptedException, ExecutionException {
+ DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient();
+ Database database = dbAdminClient.createDatabase(INSTANCE_NAME, DATABASE_NAME, Arrays.asList("CREATE TABLE TestTable (Key INT64, Value STRING(MAX)) PRIMARY KEY (Key)")).get();
+ }
+
+ private InstanceId createInstance(Spanner spanner) throws InterruptedException, ExecutionException {
+ InstanceConfigId instanceConfig = InstanceConfigId.of(PROJECT_NAME, "emulator-config");
+ InstanceId instanceId = InstanceId.of(PROJECT_NAME, INSTANCE_NAME);
+ InstanceAdminClient insAdminClient = spanner.getInstanceAdminClient();
+ Instance instance = insAdminClient.createInstance(InstanceInfo.newBuilder(instanceId).setNodeCount(1).setDisplayName("Test instance").setInstanceConfigId(instanceConfig).build()).get();
+ return instanceId;
+ }
+
+}