Skip to content

Commit 1ad2a05

Browse files
okohubrnorth
andauthored
Add Azure Cosmos DB module: Introduce CosmosDBEmulatorContainer (#4303)
Co-authored-by: Richard North <[email protected]>
1 parent 8d40cbd commit 1ad2a05

File tree

7 files changed

+319
-0
lines changed

7 files changed

+319
-0
lines changed

docs/modules/azure.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Azure Module
2+
3+
!!! note
4+
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.
5+
6+
Testcontainers module for the Microsoft Azure's [SDK](https://github.com/Azure/azure-sdk-for-java).
7+
8+
Currently, the module supports `CosmosDB` emulator. In order to use it, you should use the following class:
9+
10+
Class | Container Image
11+
-|-
12+
CosmosDBEmulatorContainer | [mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator](https://github.com/microsoft/containerregistry)
13+
14+
## Usage example
15+
16+
### CosmosDB
17+
18+
Start Azure CosmosDB Emulator during a test:
19+
20+
<!--codeinclude-->
21+
[Starting a Azure CosmosDB Emulator container](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:emulatorContainer
22+
<!--/codeinclude-->
23+
24+
Prepare KeyStore to use for SSL.
25+
26+
<!--codeinclude-->
27+
[Building KeyStore from certificate within container](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:buildAndSaveNewKeyStore
28+
<!--/codeinclude-->
29+
30+
Set system trust-store parameters to use already built KeyStore:
31+
32+
<!--codeinclude-->
33+
[Set system trust-store parameters](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:setSystemTrustStoreParameters
34+
<!--/codeinclude-->
35+
36+
Build Azure CosmosDB client:
37+
38+
<!--codeinclude-->
39+
[Build Azure CosmosDB client](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:buildClient
40+
<!--/codeinclude-->
41+
42+
Test against the Emulator:
43+
44+
<!--codeinclude-->
45+
[Testing against Azure CosmosDB Emulator container](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:testWithClientAgainstEmulatorContainer
46+
<!--/codeinclude-->
47+
48+
## Adding this module to your project dependencies
49+
50+
Add the following dependency to your `pom.xml`/`build.gradle` file:
51+
52+
```groovy tab='Gradle'
53+
testImplementation "org.testcontainers:azure:{{latest_version}}"
54+
```
55+
56+
```xml tab='Maven'
57+
<dependency>
58+
<groupId>org.testcontainers</groupId>
59+
<artifactId>azure</artifactId>
60+
<version>{{latest_version}}</version>
61+
<scope>test</scope>
62+
</dependency>
63+
```
64+

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ nav:
6161
- modules/databases/postgres.md
6262
- modules/databases/presto.md
6363
- modules/databases/trino.md
64+
- modules/azure.md
6465
- modules/docker_compose.md
6566
- modules/elasticsearch.md
6667
- modules/gcloud.md

modules/azure/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
description = "Testcontainers :: Azure"
2+
3+
dependencies {
4+
api project(':testcontainers')
5+
6+
testImplementation 'org.assertj:assertj-core:3.15.0'
7+
testImplementation 'com.azure:azure-cosmos:4.16.0'
8+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.testcontainers.containers;
2+
3+
import org.testcontainers.containers.wait.strategy.Wait;
4+
import org.testcontainers.utility.DockerImageName;
5+
6+
import java.security.KeyStore;
7+
8+
/**
9+
* An Azure CosmosDB container
10+
*/
11+
public class CosmosDBEmulatorContainer extends GenericContainer<CosmosDBEmulatorContainer> {
12+
13+
private static final DockerImageName DEFAULT_IMAGE_NAME =
14+
DockerImageName.parse("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator");
15+
16+
private static final int PORT = 8081;
17+
18+
/**
19+
* @param dockerImageName specified docker image name to run
20+
*/
21+
public CosmosDBEmulatorContainer(final DockerImageName dockerImageName) {
22+
super(dockerImageName);
23+
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
24+
withExposedPorts(PORT);
25+
waitingFor(Wait.forLogMessage("(?s).*Started\\r\\n$", 1));
26+
}
27+
28+
/**
29+
* @return new KeyStore built with PKCS12
30+
*/
31+
public KeyStore buildNewKeyStore() {
32+
return KeyStoreBuilder.buildByDownloadingCertificate(getEmulatorEndpoint(), getEmulatorKey());
33+
}
34+
35+
/**
36+
* Emulator key is a known constant and specified in Azure Cosmos DB Documents.
37+
* This key is also used as password for emulator certificate file.
38+
*
39+
* @return predefined emulator key
40+
* @see <a href="https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator?tabs=ssl-netstd21#authenticate-requests">Azure Cosmos DB Documents</a>
41+
*/
42+
public String getEmulatorKey() {
43+
return "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
44+
}
45+
46+
/**
47+
* @return secure https emulator endpoint to send requests
48+
*/
49+
public String getEmulatorEndpoint() {
50+
return "https://" + getHost() + ":" + getMappedPort(PORT);
51+
}
52+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package org.testcontainers.containers;
2+
3+
import okhttp3.Cache;
4+
import okhttp3.OkHttpClient;
5+
import okhttp3.Request;
6+
import okhttp3.Response;
7+
8+
import javax.net.ssl.SSLContext;
9+
import javax.net.ssl.SSLSocketFactory;
10+
import javax.net.ssl.TrustManager;
11+
import javax.net.ssl.X509TrustManager;
12+
import java.io.InputStream;
13+
import java.security.KeyStore;
14+
import java.security.SecureRandom;
15+
import java.security.cert.Certificate;
16+
import java.security.cert.CertificateFactory;
17+
import java.security.cert.X509Certificate;
18+
import java.util.Objects;
19+
20+
final class KeyStoreBuilder {
21+
22+
static KeyStore buildByDownloadingCertificate(String endpoint, String keyStorePassword) {
23+
OkHttpClient client = null;
24+
Response response = null;
25+
try {
26+
TrustManager[] trustAllManagers = buildTrustAllManagers();
27+
client = buildTrustAllClient(trustAllManagers);
28+
Request request = buildRequest(endpoint);
29+
response = client.newCall(request).execute();
30+
return buildKeyStore(response.body().byteStream(), keyStorePassword);
31+
} catch (Exception ex) {
32+
throw new IllegalStateException(ex);
33+
} finally {
34+
closeResponseSilently(response);
35+
closeClientSilently(client);
36+
}
37+
}
38+
39+
private static TrustManager[] buildTrustAllManagers() {
40+
return new TrustManager[] {
41+
new X509TrustManager() {
42+
@Override
43+
public void checkClientTrusted(X509Certificate[] chain, String authType) {
44+
}
45+
46+
@Override
47+
public void checkServerTrusted(X509Certificate[] chain, String authType) {
48+
}
49+
50+
@Override
51+
public X509Certificate[] getAcceptedIssuers() {
52+
return new X509Certificate[]{};
53+
}
54+
}
55+
};
56+
}
57+
58+
private static OkHttpClient buildTrustAllClient(TrustManager[] trustManagers) throws Exception {
59+
SSLContext sslContext = SSLContext.getInstance("SSL");
60+
sslContext.init(null, trustManagers, new SecureRandom());
61+
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
62+
return new OkHttpClient.Builder()
63+
.sslSocketFactory(socketFactory, (X509TrustManager) trustManagers[0])
64+
.hostnameVerifier((s, sslSession) -> true)
65+
.build();
66+
}
67+
68+
private static Request buildRequest(String endpoint) {
69+
return new Request.Builder()
70+
.get()
71+
.url(endpoint + "/_explorer/emulator.pem")
72+
.build();
73+
}
74+
75+
private static KeyStore buildKeyStore(InputStream certificateStream, String keyStorePassword) throws Exception {
76+
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(certificateStream);
77+
KeyStore keystore = KeyStore.getInstance("PKCS12");
78+
keystore.load(null, keyStorePassword.toCharArray());
79+
keystore.setCertificateEntry("azure-cosmos-emulator", certificate);
80+
return keystore;
81+
}
82+
83+
private static void closeResponseSilently(Response response) {
84+
try {
85+
if (Objects.nonNull(response)) {
86+
response.close();
87+
}
88+
} catch (Exception ignored) {
89+
}
90+
}
91+
92+
private static void closeClientSilently(OkHttpClient client) {
93+
try {
94+
if (Objects.nonNull(client)) {
95+
client.dispatcher().executorService().shutdown();
96+
client.connectionPool().evictAll();
97+
Cache cache = client.cache();
98+
if (Objects.nonNull(cache)) {
99+
cache.close();
100+
}
101+
}
102+
} catch (Exception ignored) {
103+
}
104+
}
105+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package org.testcontainers.containers;
2+
3+
import com.azure.cosmos.CosmosAsyncClient;
4+
import com.azure.cosmos.CosmosClientBuilder;
5+
import com.azure.cosmos.models.CosmosContainerResponse;
6+
import com.azure.cosmos.models.CosmosDatabaseResponse;
7+
import org.assertj.core.api.Assertions;
8+
import org.junit.AfterClass;
9+
import org.junit.BeforeClass;
10+
import org.junit.Rule;
11+
import org.junit.Test;
12+
import org.junit.rules.TemporaryFolder;
13+
import org.testcontainers.utility.DockerImageName;
14+
15+
import java.io.FileOutputStream;
16+
import java.nio.file.Path;
17+
import java.security.KeyStore;
18+
import java.util.Properties;
19+
20+
public class CosmosDBEmulatorContainerTest {
21+
22+
private static Properties originalSystemProperties;
23+
24+
@BeforeClass
25+
public static void captureOriginalSystemProperties() {
26+
originalSystemProperties = (Properties) System.getProperties().clone();
27+
}
28+
29+
@AfterClass
30+
public static void restoreOriginalSystemProperties() {
31+
System.setProperties(originalSystemProperties);
32+
}
33+
34+
@Rule
35+
public TemporaryFolder tempFolder = TemporaryFolder.builder().assureDeletion().build();
36+
37+
@Rule
38+
// emulatorContainer {
39+
public CosmosDBEmulatorContainer emulator = new CosmosDBEmulatorContainer(
40+
DockerImageName.parse("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest")
41+
);
42+
// }
43+
44+
@Test
45+
public void testWithCosmosClient() throws Exception {
46+
// buildAndSaveNewKeyStore {
47+
Path keyStoreFile = tempFolder.newFile("azure-cosmos-emulator.keystore").toPath();
48+
KeyStore keyStore = emulator.buildNewKeyStore();
49+
keyStore.store(new FileOutputStream(keyStoreFile.toFile()), emulator.getEmulatorKey().toCharArray());
50+
// }
51+
// setSystemTrustStoreParameters {
52+
System.setProperty("javax.net.ssl.trustStore", keyStoreFile.toString());
53+
System.setProperty("javax.net.ssl.trustStorePassword", emulator.getEmulatorKey());
54+
System.setProperty("javax.net.ssl.trustStoreType", "PKCS12");
55+
// }
56+
// buildClient {
57+
CosmosAsyncClient client = new CosmosClientBuilder()
58+
.gatewayMode()
59+
.endpointDiscoveryEnabled(false)
60+
.endpoint(emulator.getEmulatorEndpoint())
61+
.key(emulator.getEmulatorKey())
62+
.buildAsyncClient();
63+
// }
64+
// testWithClientAgainstEmulatorContainer {
65+
CosmosDatabaseResponse databaseResponse =
66+
client.createDatabaseIfNotExists("Azure").block();
67+
Assertions.assertThat(databaseResponse.getStatusCode()).isEqualTo(201);
68+
CosmosContainerResponse containerResponse =
69+
client.getDatabase("Azure").createContainerIfNotExists("ServiceContainer", "/name").block();
70+
Assertions.assertThat(containerResponse.getStatusCode()).isEqualTo(201);
71+
// }
72+
}
73+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<configuration>
2+
3+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
4+
<!-- encoders are assigned the type
5+
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
6+
<encoder>
7+
<pattern>%d{HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
8+
</encoder>
9+
</appender>
10+
11+
<root level="INFO">
12+
<appender-ref ref="STDOUT"/>
13+
</root>
14+
15+
<logger name="org.testcontainers" level="INFO"/>
16+
</configuration>

0 commit comments

Comments
 (0)