Skip to content

Commit

Permalink
Azure Cosmos DB: Introduce CosmosDBEmulatorContainer
Browse files Browse the repository at this point in the history
  • Loading branch information
okohub committed Jul 30, 2021
1 parent 4df9b90 commit 03c8825
Show file tree
Hide file tree
Showing 9 changed files with 430 additions and 0 deletions.
46 changes: 46 additions & 0 deletions docs/modules/azure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 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 CosmosDB Emulator during a test:

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

Test against the Emulator:

<!--codeinclude-->
[Testing with a CosmosDB Emulator container](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:testWithEmulatorContainer
<!--/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>
```

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,90 @@
package org.testcontainers.containers;

/**
* Taken from jakarta xml bind DatatypeConverter#parseBase64Binary
*
* @author Onur Kagan Ozcan
*/
final class Base64Helper {

private static final byte PADDING = 127;

private static final byte[] decodeMap = initDecodeMap();

private static byte[] initDecodeMap() {
byte[] map = new byte[128];
int i;
for (i = 0; i < 128; i++) {
map[i] = -1;
}
for (i = 'A'; i <= 'Z'; i++) {
map[i] = (byte) (i - 'A');
}
for (i = 'a'; i <= 'z'; i++) {
map[i] = (byte) (i - 'a' + 26);
}
for (i = '0'; i <= '9'; i++) {
map[i] = (byte) (i - '0' + 52);
}
map['+'] = 62;
map['/'] = 63;
map['='] = PADDING;
return map;
}

public static byte[] parseBase64Binary(String text) {
final int buflen = guessLength(text);
final byte[] out = new byte[buflen];
int o = 0;
//
final int len = text.length();
int i;
//
final byte[] quadruplet = new byte[4];
int q = 0;
for (i = 0; i < len; i++) {
char ch = text.charAt(i);
byte v = decodeMap[ch];
if (v != -1) {
quadruplet[q++] = v;
}
if (q == 4) {
out[o++] = (byte) ((quadruplet[0] << 2) | (quadruplet[1] >> 4));
if (quadruplet[2] != PADDING) {
out[o++] = (byte) ((quadruplet[1] << 4) | (quadruplet[2] >> 2));
}
if (quadruplet[3] != PADDING) {
out[o++] = (byte) ((quadruplet[2] << 6) | (quadruplet[3]));
}
q = 0;
}
}
if (buflen == o) {
return out;
}
byte[] nb = new byte[o];
System.arraycopy(out, 0, nb, 0, o);
return nb;
}

private static int guessLength(String text) {
final int len = text.length();
int j = len - 1;
for (; j >= 0; j--) {
byte code = decodeMap[text.charAt(j)];
if (code == PADDING) {
continue;
}
if (code == -1) {
return text.length() / 4 * 3;
}
break;
}
j++;
int padSize = len - j;
if (padSize > 2) {
return text.length() / 4 * 3;
}
return text.length() / 4 * 3 - padSize;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.testcontainers.containers;

/**
* @author Onur Kagan Ozcan
*/
final class Constants {

static final String EMULATOR_KEY =
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";

static final String TEMP_DIRECTORY_NAME = "azure-cosmosdb-emulator-temp";

static final String EMULATOR_CERTIFICATE_ENDPOINT_URI = "/_explorer/emulator.pem";

static final String EMULATOR_CERTIFICATE_FILE_NAME = "emulator.pem";

static final String EMULATOR_CERTIFICATE_ALIAS = "emulator-cert";

static final String KEYSTORE_FILE_NAME = "cosmos_emulator.keystore";

static final String STORE_TYPE = "PKCS12";

static final String STORE_PASSWORD = "changeit";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.testcontainers.containers;

import com.github.dockerjava.api.command.InspectContainerResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
import org.testcontainers.utility.DockerImageName;

import static org.testcontainers.containers.Constants.EMULATOR_CERTIFICATE_FILE_NAME;
import static org.testcontainers.containers.Constants.EMULATOR_KEY;
import static org.testcontainers.containers.Constants.KEYSTORE_FILE_NAME;
import static org.testcontainers.containers.Constants.STORE_PASSWORD;
import static org.testcontainers.containers.Constants.STORE_TYPE;
import static org.testcontainers.containers.Constants.TEMP_DIRECTORY_NAME;
import static org.testcontainers.utility.DockerImageName.parse;

/**
* An Azure CosmosDB container
*
* Default port is 8081.
*
* @author Onur Kagan Ozcan
*/
public class CosmosDBEmulatorContainer extends GenericContainer<CosmosDBEmulatorContainer> {

public static final String LINUX_AZURE_COSMOS_DB_EMULATOR = "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator";

private static final DockerImageName DEFAULT_IMAGE_NAME = parse(LINUX_AZURE_COSMOS_DB_EMULATOR);

private static final int PORT = 8081;

private Path tempDirectory;

private Boolean autoSetSystemTrustStoreParameters = true;

public CosmosDBEmulatorContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
withExposedPorts(PORT);
setWaitStrategy(new LogMessageWaitStrategy().withRegEx("(?s).*Started\\r\\n$"));
}

@Override
protected void configure() {
try {
this.tempDirectory = Files.createTempDirectory(TEMP_DIRECTORY_NAME);
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}

@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
Path emulatorCertificatePath = tempDirectory.resolve(EMULATOR_CERTIFICATE_FILE_NAME);
KeyStoreUtils.downloadPemFromEmulator(getEmulatorEndpoint(), emulatorCertificatePath);
Path keyStorePath = tempDirectory.resolve(KEYSTORE_FILE_NAME);
KeyStoreUtils.importEmulatorCertificate(emulatorCertificatePath, keyStorePath);
if (autoSetSystemTrustStoreParameters) {
setSystemTrustStoreParameters(keyStorePath.toFile().getAbsolutePath(), STORE_PASSWORD, STORE_TYPE);
}
}

/**
* Disable system property set for further customizations.
* You can still set with public method
*
* @see CosmosDBEmulatorContainer#setSystemTrustStoreParameters(String, String, String)
* @return current instance
*/
public CosmosDBEmulatorContainer withDisablingAutoSetSystemTrustStoreParameters() {
this.autoSetSystemTrustStoreParameters = false;
return this;
}

/**
* @param trustStore keyStore path
* @param trustStorePassword keyStore file password
* @param trustStoreType keyStore type e.g PKCS12, JKS
*/
public void setSystemTrustStoreParameters(String trustStore, String trustStorePassword, String trustStoreType) {
System.setProperty("javax.net.ssl.trustStore", trustStore);
System.setProperty("javax.net.ssl.trustStorePassword", trustStorePassword);
System.setProperty("javax.net.ssl.trustStoreType", trustStoreType);
}

/**
* @return endpoint to use in CosmosClient/CosmosAsyncClient objects
*/
public String getEmulatorEndpoint() {
return "https://" + getHost() + ":" + getMappedPort(PORT);
}

/**
* @return default local key of emulator as defined in Azure Cosmos DB docs and examples
*/
public String getEmulatorLocalKey() {
return EMULATOR_KEY;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.testcontainers.containers;

import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

import static java.nio.file.Files.readAllBytes;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static org.testcontainers.containers.Constants.EMULATOR_CERTIFICATE_ALIAS;
import static org.testcontainers.containers.Constants.EMULATOR_CERTIFICATE_ENDPOINT_URI;
import static org.testcontainers.containers.Constants.STORE_PASSWORD;
import static org.testcontainers.containers.Constants.STORE_TYPE;

/**
* @author Onur Kagan Ozcan
*/
final class KeyStoreUtils {

static void downloadPemFromEmulator(String endpoint, Path pemResourceOutput) {
try {
URLSSLCertificateCheck.disable();
try (InputStream in = new URL(endpoint + EMULATOR_CERTIFICATE_ENDPOINT_URI).openStream()) {
Files.copy(in, pemResourceOutput, REPLACE_EXISTING);
}
} catch (Exception ex) {
throw new IllegalStateException();
} finally {
URLSSLCertificateCheck.enable();
}
}

static void importEmulatorCertificate(Path pemLocation, Path keyStoreOutput) {
try {
byte[] emulatorPemFile = readAllBytes(pemLocation);
byte[] emulatorCertificate = parseDERFromPEM(emulatorPemFile);
X509Certificate theCertificateObject = generateCertificateFromDER(emulatorCertificate);
//
KeyStore keystore = KeyStore.getInstance(STORE_TYPE);
keystore.load(null, STORE_PASSWORD.toCharArray());
keystore.setCertificateEntry(EMULATOR_CERTIFICATE_ALIAS, theCertificateObject);
keystore.store(new FileOutputStream(keyStoreOutput.toFile()), STORE_PASSWORD.toCharArray());
} catch (Exception ex) {
throw new IllegalStateException();
}
}

private static byte[] parseDERFromPEM(byte[] pem) {
String data = new String(pem);
String[] tokens = data.split("-----BEGIN CERTIFICATE-----");
tokens = tokens[1].split("-----END CERTIFICATE-----");
return Base64Helper.parseBase64Binary(tokens[0]);
}

private static X509Certificate generateCertificateFromDER(byte[] certBytes) throws CertificateException {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certBytes));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.testcontainers.containers;

import java.security.cert.X509Certificate;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

/**
* @author Onur Kagan Ozcan
*/
final class URLSSLCertificateCheck {

static void disable() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}

@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) {
// Not implemented
}

@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) {
// Not implemented
}
}};
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}

static void enable() {
HttpsURLConnection.setDefaultSSLSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
}
}
Loading

0 comments on commit 03c8825

Please sign in to comment.