Skip to content

Add GCloud module for Google Cloud Datastore, Firestore, PubSub, and Spanner emulators #2690

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Oct 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/modules/gcloud.md
Original file line number Diff line number Diff line change
@@ -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:

<!--codeinclude-->
[Creating a Datastore container](../../modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java) inside_block:creatingDatastoreEmulatorContainer
<!--/codeinclude-->

And how to start it:

<!--codeinclude-->
[Starting a Datastore container](../../modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java) inside_block:startingDatastoreEmulatorContainer
<!--/codeinclude-->

## 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'
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>gcloud</artifactId>
<version>{{latest_version}}</version>
<scope>test</scope>
</dependency>
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions modules/gcloud/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
@@ -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<DatastoreEmulatorContainer> {

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);
}

}
Original file line number Diff line number Diff line change
@@ -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<FirestoreEmulatorContainer> {

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);
}

}
Original file line number Diff line number Diff line change
@@ -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<PubSubEmulatorContainer> {

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);
}

}
Original file line number Diff line number Diff line change
@@ -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<SpannerEmulatorContainer> {

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;
Comment on lines +15 to +16
Copy link
Member

Choose a reason for hiding this comment

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

I wonder, maybe these should be public constants so that they can be used by tests? If we did that, we could do so for the other container classes in this PR.

WDYT @bsideup, @kiview?

Copy link
Member

Choose a reason for hiding this comment

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

WDYT about not exposing the constants, but getGrpcPort/getHttpPort methods?

Copy link
Contributor

Choose a reason for hiding this comment

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

looking at some of the other modules, private static final seems like the norm/convention atm. would it make sense to follow up w/ an issue for the broader change to expose these across modules?

Copy link
Member

Choose a reason for hiding this comment

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

I've been persuaded by @bsideup - many of our other modules have some mechanism for getting the mapped ports, address or URL for the running container. It seems like we should do this here, instead of exposing constants, as it's fundamentally more useful for the user.

I'll take the action to do this, as we've asked @eddumelendez to do too much already. I'll raise a quick PR tonight, and will merge this PR now.


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\\..*"));
}

}
Original file line number Diff line number Diff line change
@@ -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");
}
// }

}
Original file line number Diff line number Diff line change
@@ -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<String, Object> data = new HashMap<>();
data.put("first", "Ada");
data.put("last", "Lovelace");
ApiFuture<WriteResult> result = docRef.set(data);
result.get();

ApiFuture<QuerySnapshot> query = users.get();
QuerySnapshot querySnapshot = query.get();

assertThat(querySnapshot.getDocuments().get(0).getData()).containsEntry("first", "Ada");
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Loading