Skip to content

Commit

Permalink
Add Azure Cosmos DB module: Introduce CosmosDBEmulatorContainer (#4303)
Browse files Browse the repository at this point in the history
Co-authored-by: Richard North <[email protected]>
  • Loading branch information
okohub and rnorth authored Oct 12, 2021
1 parent 8d40cbd commit 1ad2a05
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 0 deletions.
64 changes: 64 additions & 0 deletions docs/modules/azure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Azure 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 Microsoft Azure's [SDK](https://github.com/Azure/azure-sdk-for-java).

Currently, the module supports `CosmosDB` emulator. In order to use it, you should use the following class:

Class | Container Image
-|-
CosmosDBEmulatorContainer | [mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator](https://github.com/microsoft/containerregistry)

## Usage example

### CosmosDB

Start Azure CosmosDB Emulator during a test:

<!--codeinclude-->
[Starting a Azure CosmosDB Emulator container](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:emulatorContainer
<!--/codeinclude-->

Prepare KeyStore to use for SSL.

<!--codeinclude-->
[Building KeyStore from certificate within container](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:buildAndSaveNewKeyStore
<!--/codeinclude-->

Set system trust-store parameters to use already built KeyStore:

<!--codeinclude-->
[Set system trust-store parameters](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:setSystemTrustStoreParameters
<!--/codeinclude-->

Build Azure CosmosDB client:

<!--codeinclude-->
[Build Azure CosmosDB client](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:buildClient
<!--/codeinclude-->

Test against the Emulator:

<!--codeinclude-->
[Testing against Azure CosmosDB Emulator container](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:testWithClientAgainstEmulatorContainer
<!--/codeinclude-->

## Adding this module to your project dependencies

Add the following dependency to your `pom.xml`/`build.gradle` file:

```groovy tab='Gradle'
testImplementation "org.testcontainers:azure:{{latest_version}}"
```

```xml tab='Maven'
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>azure</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 @@ -61,6 +61,7 @@ nav:
- modules/databases/postgres.md
- modules/databases/presto.md
- modules/databases/trino.md
- modules/azure.md
- modules/docker_compose.md
- modules/elasticsearch.md
- modules/gcloud.md
Expand Down
8 changes: 8 additions & 0 deletions modules/azure/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
description = "Testcontainers :: Azure"

dependencies {
api project(':testcontainers')

testImplementation 'org.assertj:assertj-core:3.15.0'
testImplementation 'com.azure:azure-cosmos:4.16.0'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.testcontainers.containers;

import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;

import java.security.KeyStore;

/**
* An Azure CosmosDB container
*/
public class CosmosDBEmulatorContainer extends GenericContainer<CosmosDBEmulatorContainer> {

private static final DockerImageName DEFAULT_IMAGE_NAME =
DockerImageName.parse("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator");

private static final int PORT = 8081;

/**
* @param dockerImageName specified docker image name to run
*/
public CosmosDBEmulatorContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
withExposedPorts(PORT);
waitingFor(Wait.forLogMessage("(?s).*Started\\r\\n$", 1));
}

/**
* @return new KeyStore built with PKCS12
*/
public KeyStore buildNewKeyStore() {
return KeyStoreBuilder.buildByDownloadingCertificate(getEmulatorEndpoint(), getEmulatorKey());
}

/**
* Emulator key is a known constant and specified in Azure Cosmos DB Documents.
* This key is also used as password for emulator certificate file.
*
* @return predefined emulator key
* @see <a href="https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator?tabs=ssl-netstd21#authenticate-requests">Azure Cosmos DB Documents</a>
*/
public String getEmulatorKey() {
return "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
}

/**
* @return secure https emulator endpoint to send requests
*/
public String getEmulatorEndpoint() {
return "https://" + getHost() + ":" + getMappedPort(PORT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.testcontainers.containers;

import okhttp3.Cache;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Objects;

final class KeyStoreBuilder {

static KeyStore buildByDownloadingCertificate(String endpoint, String keyStorePassword) {
OkHttpClient client = null;
Response response = null;
try {
TrustManager[] trustAllManagers = buildTrustAllManagers();
client = buildTrustAllClient(trustAllManagers);
Request request = buildRequest(endpoint);
response = client.newCall(request).execute();
return buildKeyStore(response.body().byteStream(), keyStorePassword);
} catch (Exception ex) {
throw new IllegalStateException(ex);
} finally {
closeResponseSilently(response);
closeClientSilently(client);
}
}

private static TrustManager[] buildTrustAllManagers() {
return new TrustManager[] {
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
};
}

private static OkHttpClient buildTrustAllClient(TrustManager[] trustManagers) throws Exception {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustManagers, new SecureRandom());
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
return new OkHttpClient.Builder()
.sslSocketFactory(socketFactory, (X509TrustManager) trustManagers[0])
.hostnameVerifier((s, sslSession) -> true)
.build();
}

private static Request buildRequest(String endpoint) {
return new Request.Builder()
.get()
.url(endpoint + "/_explorer/emulator.pem")
.build();
}

private static KeyStore buildKeyStore(InputStream certificateStream, String keyStorePassword) throws Exception {
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(certificateStream);
KeyStore keystore = KeyStore.getInstance("PKCS12");
keystore.load(null, keyStorePassword.toCharArray());
keystore.setCertificateEntry("azure-cosmos-emulator", certificate);
return keystore;
}

private static void closeResponseSilently(Response response) {
try {
if (Objects.nonNull(response)) {
response.close();
}
} catch (Exception ignored) {
}
}

private static void closeClientSilently(OkHttpClient client) {
try {
if (Objects.nonNull(client)) {
client.dispatcher().executorService().shutdown();
client.connectionPool().evictAll();
Cache cache = client.cache();
if (Objects.nonNull(cache)) {
cache.close();
}
}
} catch (Exception ignored) {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.testcontainers.containers;

import com.azure.cosmos.CosmosAsyncClient;
import com.azure.cosmos.CosmosClientBuilder;
import com.azure.cosmos.models.CosmosContainerResponse;
import com.azure.cosmos.models.CosmosDatabaseResponse;
import org.assertj.core.api.Assertions;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.testcontainers.utility.DockerImageName;

import java.io.FileOutputStream;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.Properties;

public class CosmosDBEmulatorContainerTest {

private static Properties originalSystemProperties;

@BeforeClass
public static void captureOriginalSystemProperties() {
originalSystemProperties = (Properties) System.getProperties().clone();
}

@AfterClass
public static void restoreOriginalSystemProperties() {
System.setProperties(originalSystemProperties);
}

@Rule
public TemporaryFolder tempFolder = TemporaryFolder.builder().assureDeletion().build();

@Rule
// emulatorContainer {
public CosmosDBEmulatorContainer emulator = new CosmosDBEmulatorContainer(
DockerImageName.parse("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest")
);
// }

@Test
public void testWithCosmosClient() throws Exception {
// buildAndSaveNewKeyStore {
Path keyStoreFile = tempFolder.newFile("azure-cosmos-emulator.keystore").toPath();
KeyStore keyStore = emulator.buildNewKeyStore();
keyStore.store(new FileOutputStream(keyStoreFile.toFile()), emulator.getEmulatorKey().toCharArray());
// }
// setSystemTrustStoreParameters {
System.setProperty("javax.net.ssl.trustStore", keyStoreFile.toString());
System.setProperty("javax.net.ssl.trustStorePassword", emulator.getEmulatorKey());
System.setProperty("javax.net.ssl.trustStoreType", "PKCS12");
// }
// buildClient {
CosmosAsyncClient client = new CosmosClientBuilder()
.gatewayMode()
.endpointDiscoveryEnabled(false)
.endpoint(emulator.getEmulatorEndpoint())
.key(emulator.getEmulatorKey())
.buildAsyncClient();
// }
// testWithClientAgainstEmulatorContainer {
CosmosDatabaseResponse databaseResponse =
client.createDatabaseIfNotExists("Azure").block();
Assertions.assertThat(databaseResponse.getStatusCode()).isEqualTo(201);
CosmosContainerResponse containerResponse =
client.getDatabase("Azure").createContainerIfNotExists("ServiceContainer", "/name").block();
Assertions.assertThat(containerResponse.getStatusCode()).isEqualTo(201);
// }
}
}
16 changes: 16 additions & 0 deletions modules/azure/src/test/resources/logback-test.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>

<logger name="org.testcontainers" level="INFO"/>
</configuration>

0 comments on commit 1ad2a05

Please sign in to comment.