+ * {@link #withCommand(String...)} cannot be used for this: {@link #configure()} always
+ * rebuilds the process command and overwrites any previously set command.
+ *
+ * @param options additional Azurite flags, for example {@code --skipApiVersionCheck}
+ * @return this
+ */
+ public AzuriteContainer withCommandOptions(String... options) {
+ this.commandOptions.addAll(Arrays.asList(options));
+ return this;
+ }
+
+ @Override
+ protected void configure() {
+ withCommand(getCommandLine());
+ if (this.cert != null) {
+ logger().info("Using path for cert file: '{}'", this.cert);
+ withCopyFileToContainer(this.cert, "/cert" + this.certExtension);
+ if (this.key != null) {
+ logger().info("Using path for key file: '{}'", this.key);
+ withCopyFileToContainer(this.key, "/key.pem");
+ }
+ }
+ }
+
+ /**
+ * Returns the connection string for the default credentials.
+ *
+ * @return connection string
+ */
+ public String getConnectionString() {
+ return getConnectionString(WELL_KNOWN_ACCOUNT_NAME, WELL_KNOWN_ACCOUNT_KEY);
+ }
+
+ /**
+ * Returns the connection string for the account name and key specified.
+ *
+ * @param accountName The name of the account
+ * @param accountKey The account key
+ * @return connection string
+ */
+ public String getConnectionString(final String accountName, final String accountKey) {
+ final String protocol = cert != null ? "https" : "http";
+ return String.format(
+ CONNECTION_STRING_FORMAT,
+ protocol,
+ accountName,
+ accountKey,
+ protocol,
+ getHost(),
+ getMappedPort(DEFAULT_BLOB_PORT),
+ accountName,
+ protocol,
+ getHost(),
+ getMappedPort(DEFAULT_QUEUE_PORT),
+ accountName,
+ protocol,
+ getHost(),
+ getMappedPort(DEFAULT_TABLE_PORT),
+ accountName
+ );
+ }
+
+ String getCommandLine() {
+ final StringBuilder args = new StringBuilder("azurite");
+ args.append(" --blobHost ").append(ALLOW_ALL_CONNECTIONS);
+ args.append(" --queueHost ").append(ALLOW_ALL_CONNECTIONS);
+ args.append(" --tableHost ").append(ALLOW_ALL_CONNECTIONS);
+ if (this.cert != null) {
+ args.append(" --cert ").append("/cert").append(this.certExtension);
+ if (this.pwd != null) {
+ args.append(" --pwd ").append(this.pwd);
+ } else {
+ args.append(" --key ").append("/key.pem");
+ }
+ }
+ for (String option : this.commandOptions) {
+ args.append(" ").append(option);
+ }
+ final String cmd = args.toString();
+ logger().debug("Using command line: '{}'", cmd);
+ return cmd;
+ }
+}
diff --git a/modules/azure/src/main/java/org/testcontainers/azure/EventHubsEmulatorContainer.java b/modules/azure/src/main/java/org/testcontainers/azure/EventHubsEmulatorContainer.java
new file mode 100644
index 00000000000..257f71a1424
--- /dev/null
+++ b/modules/azure/src/main/java/org/testcontainers/azure/EventHubsEmulatorContainer.java
@@ -0,0 +1,109 @@
+package org.testcontainers.azure;
+
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.images.builder.Transferable;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.LicenseAcceptance;
+
+/**
+ * Testcontainers implementation for Azure Eventhubs Emulator.
+ *
+ * Supported image: {@code "mcr.microsoft.com/azure-messaging/eventhubs-emulator"}
+ *
{
+
+ private static final int DEFAULT_AMQP_PORT = 5672;
+
+ private static final String CONNECTION_STRING_FORMAT =
+ "Endpoint=sb://%s:%d;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;";
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(
+ "mcr.microsoft.com/azure-messaging/eventhubs-emulator"
+ );
+
+ private AzuriteContainer azuriteContainer;
+
+ /**
+ * @param dockerImageName specified docker image name to run
+ */
+ public EventHubsEmulatorContainer(final String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ /**
+ * @param dockerImageName specified docker image name to run
+ */
+ public EventHubsEmulatorContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ waitingFor(Wait.forLogMessage(".*Emulator Service is Successfully Up!.*", 1));
+ withExposedPorts(DEFAULT_AMQP_PORT);
+ }
+
+ /**
+ * * Sets the Azurite dependency needed by the Event Hubs Container,
+ *
+ * @param azuriteContainer The Azurite container used by Event HUbs as a dependency
+ * @return this
+ */
+ public EventHubsEmulatorContainer withAzuriteContainer(final AzuriteContainer azuriteContainer) {
+ this.azuriteContainer = azuriteContainer;
+ dependsOn(this.azuriteContainer);
+ return this;
+ }
+
+ /**
+ * Provide the broker configuration to the container.
+ *
+ * @param config The file containing the broker configuration
+ * @return this
+ */
+ public EventHubsEmulatorContainer withConfig(final Transferable config) {
+ withCopyToContainer(config, "/Eventhubs_Emulator/ConfigFiles/Config.json");
+ return this;
+ }
+
+ /**
+ * Accepts the EULA of the container.
+ *
+ * @return this
+ */
+ public EventHubsEmulatorContainer acceptLicense() {
+ withEnv("ACCEPT_EULA", "Y");
+ return this;
+ }
+
+ @Override
+ protected void configure() {
+ if (azuriteContainer == null) {
+ throw new IllegalStateException(
+ "The image " +
+ getDockerImageName() +
+ " requires an Azurite container. Please provide one with the withAzuriteContainer method!"
+ );
+ }
+ final String azuriteHost = azuriteContainer.getNetworkAliases().get(0);
+ withEnv("BLOB_SERVER", azuriteHost);
+ withEnv("METADATA_SERVER", azuriteHost);
+ // If license was not accepted programmatically, check if it was accepted via resource file
+ if (!getEnvMap().containsKey("ACCEPT_EULA")) {
+ LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName());
+ acceptLicense();
+ }
+ }
+
+ /**
+ * Returns the connection string.
+ *
+ * @return connection string
+ */
+ public String getConnectionString() {
+ return String.format(CONNECTION_STRING_FORMAT, getHost(), getMappedPort(DEFAULT_AMQP_PORT));
+ }
+}
diff --git a/modules/azure/src/main/java/org/testcontainers/azure/ServiceBusEmulatorContainer.java b/modules/azure/src/main/java/org/testcontainers/azure/ServiceBusEmulatorContainer.java
new file mode 100644
index 00000000000..4557b6acb6a
--- /dev/null
+++ b/modules/azure/src/main/java/org/testcontainers/azure/ServiceBusEmulatorContainer.java
@@ -0,0 +1,107 @@
+package org.testcontainers.azure;
+
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.images.builder.Transferable;
+import org.testcontainers.mssqlserver.MSSQLServerContainer;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.LicenseAcceptance;
+
+/**
+ * Testcontainers implementation for Azure Service Bus Emulator.
+ *
+ * Supported image: {@code mcr.microsoft.com/azure-messaging/servicebus-emulator}
+ *
+ * Exposed port: 5672
+ */
+public class ServiceBusEmulatorContainer extends GenericContainer {
+
+ private static final String CONNECTION_STRING_FORMAT =
+ "Endpoint=sb://%s:%d;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;";
+
+ private static final int DEFAULT_PORT = 5672;
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(
+ "mcr.microsoft.com/azure-messaging/servicebus-emulator"
+ );
+
+ private MSSQLServerContainer msSqlServerContainer;
+
+ /**
+ * @param dockerImageName The specified docker image name to run
+ */
+ public ServiceBusEmulatorContainer(final String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ /**
+ * @param dockerImageName The specified docker image name to run
+ */
+ public ServiceBusEmulatorContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ withExposedPorts(DEFAULT_PORT);
+ withEnv("SQL_WAIT_INTERVAL", "0");
+ waitingFor(Wait.forLogMessage(".*Emulator Service is Successfully Up!.*", 1));
+ }
+
+ /**
+ * Sets the MS SQL Server dependency needed by the Service Bus Container,
+ *
+ * @param msSqlServerContainer The MS SQL Server container used by Service Bus as a dependency
+ * @return this
+ */
+ public ServiceBusEmulatorContainer withMsSqlServerContainer(final MSSQLServerContainer msSqlServerContainer) {
+ dependsOn(msSqlServerContainer);
+ this.msSqlServerContainer = msSqlServerContainer;
+ return this;
+ }
+
+ /**
+ * Provide the Service Bus configuration JSON.
+ *
+ * @param config The configuration
+ * @return this
+ */
+ public ServiceBusEmulatorContainer withConfig(final Transferable config) {
+ withCopyToContainer(config, "/ServiceBus_Emulator/ConfigFiles/Config.json");
+ return this;
+ }
+
+ /**
+ * Accepts the EULA of the container.
+ *
+ * @return this
+ */
+ public ServiceBusEmulatorContainer acceptLicense() {
+ withEnv("ACCEPT_EULA", "Y");
+ return this;
+ }
+
+ @Override
+ protected void configure() {
+ if (msSqlServerContainer == null) {
+ throw new IllegalStateException(
+ "The image " +
+ getDockerImageName() +
+ " requires a Microsoft SQL Server container. Please provide one with the withMsSqlServerContainer method!"
+ );
+ }
+ withEnv("SQL_SERVER", msSqlServerContainer.getNetworkAliases().get(0));
+ withEnv("MSSQL_SA_PASSWORD", msSqlServerContainer.getPassword());
+ // If license was not accepted programmatically, check if it was accepted via resource file
+ if (!getEnvMap().containsKey("ACCEPT_EULA")) {
+ LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName());
+ acceptLicense();
+ }
+ }
+
+ /**
+ * Returns the connection string.
+ *
+ * @return connection string
+ */
+ public String getConnectionString() {
+ return String.format(CONNECTION_STRING_FORMAT, getHost(), getMappedPort(DEFAULT_PORT));
+ }
+}
diff --git a/modules/azure/src/main/java/org/testcontainers/containers/CosmosDBEmulatorContainer.java b/modules/azure/src/main/java/org/testcontainers/containers/CosmosDBEmulatorContainer.java
index 57af95ef106..a1cfdeaf124 100644
--- a/modules/azure/src/main/java/org/testcontainers/containers/CosmosDBEmulatorContainer.java
+++ b/modules/azure/src/main/java/org/testcontainers/containers/CosmosDBEmulatorContainer.java
@@ -6,7 +6,11 @@
import java.security.KeyStore;
/**
- * An Azure CosmosDB container
+ * Testcontainers implementation for CosmosDB Emulator.
+ *
+ * Supported image: {@code mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator}
+ *
+ * Exposed ports: 8081
*/
public class CosmosDBEmulatorContainer extends GenericContainer {
@@ -23,7 +27,7 @@ public CosmosDBEmulatorContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
withExposedPorts(PORT);
- waitingFor(Wait.forLogMessage("(?s).*Started\\r\\n$", 1));
+ waitingFor(Wait.forLogMessage(".*Started\\r\\n$", 1));
}
/**
diff --git a/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerCommandTest.java b/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerCommandTest.java
new file mode 100644
index 00000000000..c12e77ed69a
--- /dev/null
+++ b/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerCommandTest.java
@@ -0,0 +1,62 @@
+package org.testcontainers.azure;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.utility.MountableFile;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AzuriteContainerCommandTest {
+
+ private static final String IMAGE = "mcr.microsoft.com/azure-storage/azurite:3.33.0";
+
+ @Test
+ void commandLineOmitsExtraOptionsByDefault() {
+ AzuriteContainer emulator = new AzuriteContainer(IMAGE);
+
+ assertThat(emulator.getCommandLine()).doesNotContain("--skipApiVersionCheck");
+ }
+
+ @Test
+ void commandLineIncludesExtraOptions() {
+ // commandOptions {
+ AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")
+ .withCommandOptions("--skipApiVersionCheck");
+ // }
+
+ assertThat(emulator.getCommandLine())
+ .startsWith("azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0")
+ .endsWith("--skipApiVersionCheck");
+ }
+
+ @Test
+ void commandLineAppendsMultipleOptions() {
+ AzuriteContainer emulator = new AzuriteContainer(IMAGE)
+ .withCommandOptions("--skipApiVersionCheck", "--disableProductStyleUrl");
+
+ assertThat(emulator.getCommandLine()).endsWith("--skipApiVersionCheck --disableProductStyleUrl");
+ }
+
+ @Test
+ void commandLineKeepsExtraOptionsTogetherWithSsl() {
+ AzuriteContainer emulator = new AzuriteContainer(IMAGE)
+ .withSsl(MountableFile.forClasspathResource("/keystore.pfx"), "changeit")
+ .withCommandOptions("--skipApiVersionCheck");
+
+ assertThat(emulator.getCommandLine())
+ .contains("--cert /cert.pfx")
+ .endsWith("--pwd changeit --skipApiVersionCheck");
+ }
+
+ @Test
+ void configureAppliesCommandOptionsEvenIfWithCommandWasUsed() {
+ AzuriteContainer emulator = new AzuriteContainer(IMAGE)
+ .withCommand("azurite --ignored")
+ .withCommandOptions("--skipApiVersionCheck");
+
+ emulator.configure();
+
+ assertThat(String.join(" ", emulator.getCommandParts()))
+ .isEqualTo("azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --skipApiVersionCheck");
+ assertThat(emulator.getCommandLine()).contains("--blobHost 0.0.0.0").contains("--skipApiVersionCheck");
+ }
+}
diff --git a/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java b/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java
new file mode 100644
index 00000000000..5acf2fe5ad5
--- /dev/null
+++ b/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java
@@ -0,0 +1,266 @@
+package org.testcontainers.azure;
+
+import com.azure.core.util.BinaryData;
+import com.azure.data.tables.TableClient;
+import com.azure.data.tables.TableServiceClient;
+import com.azure.data.tables.TableServiceClientBuilder;
+import com.azure.storage.blob.BlobClient;
+import com.azure.storage.blob.BlobContainerClient;
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.storage.queue.QueueClient;
+import com.azure.storage.queue.QueueServiceClient;
+import com.azure.storage.queue.QueueServiceClientBuilder;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.utility.MountableFile;
+
+import java.util.Properties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AzuriteContainerTest {
+
+ private static final String PASSWORD = "changeit";
+
+ private static Properties originalSystemProperties;
+
+ @BeforeAll
+ public static void captureOriginalSystemProperties() {
+ originalSystemProperties = (Properties) System.getProperties().clone();
+ System.setProperty(
+ "javax.net.ssl.trustStore",
+ MountableFile.forClasspathResource("/keystore.pfx").getFilesystemPath()
+ );
+ System.setProperty("javax.net.ssl.trustStorePassword", PASSWORD);
+ System.setProperty("javax.net.ssl.trustStoreType", "PKCS12");
+ }
+
+ @AfterAll
+ public static void restoreOriginalSystemProperties() {
+ System.setProperties(originalSystemProperties);
+ }
+
+ @Test
+ void testWithBlobServiceClient() {
+ try (
+ // emulatorContainer {
+ AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")
+ // }
+ ) {
+ emulator.start();
+ assertThat(emulator.getConnectionString()).contains("BlobEndpoint=http://");
+ testBlob(emulator);
+ }
+ }
+
+ @Test
+ void testWithQueueServiceClient() {
+ try (AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")) {
+ emulator.start();
+ assertThat(emulator.getConnectionString()).contains("QueueEndpoint=http://");
+ testQueue(emulator);
+ }
+ }
+
+ @Test
+ void testWithTableServiceClient() {
+ try (AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")) {
+ emulator.start();
+ assertThat(emulator.getConnectionString()).contains("TableEndpoint=http://");
+ testTable(emulator);
+ }
+ }
+
+ @Test
+ void testWithBlobServiceClientWithSslUsingPfx() {
+ try (
+ AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")
+ .withSsl(MountableFile.forClasspathResource("/keystore.pfx"), PASSWORD)
+ ) {
+ emulator.start();
+ assertThat(emulator.getConnectionString()).contains("BlobEndpoint=https://");
+ testBlob(emulator);
+ }
+ }
+
+ @Test
+ void testWithQueueServiceClientWithSslUsingPfx() {
+ try (
+ AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")
+ .withSsl(MountableFile.forClasspathResource("/keystore.pfx"), PASSWORD)
+ ) {
+ emulator.start();
+ assertThat(emulator.getConnectionString()).contains("QueueEndpoint=https://");
+ testQueue(emulator);
+ }
+ }
+
+ @Test
+ void testWithTableServiceClientWithSslUsingPfx() {
+ try (
+ AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")
+ .withSsl(MountableFile.forClasspathResource("/keystore.pfx"), PASSWORD)
+ ) {
+ emulator.start();
+ assertThat(emulator.getConnectionString()).contains("TableEndpoint=https://");
+ testTable(emulator);
+ }
+ }
+
+ @Test
+ void testWithBlobServiceClientWithSslUsingPem() {
+ try (
+ AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")
+ .withSsl(
+ MountableFile.forClasspathResource("/certificate.pem"),
+ MountableFile.forClasspathResource("/key.pem")
+ )
+ ) {
+ emulator.start();
+ assertThat(emulator.getConnectionString()).contains("BlobEndpoint=https://");
+ testBlob(emulator);
+ }
+ }
+
+ @Test
+ void testWithQueueServiceClientWithSslUsingPem() {
+ try (
+ AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")
+ .withSsl(
+ MountableFile.forClasspathResource("/certificate.pem"),
+ MountableFile.forClasspathResource("/key.pem")
+ )
+ ) {
+ emulator.start();
+ assertThat(emulator.getConnectionString()).contains("QueueEndpoint=https://");
+ testQueue(emulator);
+ }
+ }
+
+ @Test
+ void testWithTableServiceClientWithSslUsingPem() {
+ try (
+ AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")
+ .withSsl(
+ MountableFile.forClasspathResource("/certificate.pem"),
+ MountableFile.forClasspathResource("/key.pem")
+ )
+ ) {
+ emulator.start();
+ assertThat(emulator.getConnectionString()).contains("TableEndpoint=https://");
+ testTable(emulator);
+ }
+ }
+
+ @Test
+ void testTwoAccountKeysWithBlobServiceClient() {
+ try (
+ // withTwoAccountKeys {
+ AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")
+ .withEnv("AZURITE_ACCOUNTS", "account1:key1:key2")
+ // }
+ ) {
+ emulator.start();
+
+ String connectionString1 = emulator.getConnectionString("account1", "key1");
+ // the second account will have access to the same container using a different key
+ String connectionString2 = emulator.getConnectionString("account1", "key2");
+
+ BlobServiceClient blobServiceClient1 = new BlobServiceClientBuilder()
+ .connectionString(connectionString1)
+ .buildClient();
+
+ BlobContainerClient containerClient1 = blobServiceClient1.createBlobContainer("test-container");
+ BlobClient blobClient1 = containerClient1.getBlobClient("test-blob.txt");
+ blobClient1.upload(BinaryData.fromString("content"));
+ boolean existsWithAccount1 = blobClient1.exists();
+ String contentWithAccount1 = blobClient1.downloadContent().toString();
+
+ BlobServiceClient blobServiceClient2 = new BlobServiceClientBuilder()
+ .connectionString(connectionString2)
+ .buildClient();
+ BlobContainerClient containerClient2 = blobServiceClient2.getBlobContainerClient("test-container");
+ BlobClient blobClient2 = containerClient2.getBlobClient("test-blob.txt");
+ boolean existsWithAccount2 = blobClient2.exists();
+ String contentWithAccount2 = blobClient2.downloadContent().toString();
+
+ assertThat(existsWithAccount1).isTrue();
+ assertThat(contentWithAccount1).isEqualTo("content");
+ assertThat(existsWithAccount2).isTrue();
+ assertThat(contentWithAccount2).isEqualTo("content");
+ }
+ }
+
+ @Test
+ void testMultipleAccountsWithBlobServiceClient() {
+ try (
+ // withMoreAccounts {
+ AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")
+ .withEnv("AZURITE_ACCOUNTS", "account1:key1;account2:key2")
+ // }
+ ) {
+ emulator.start();
+
+ // useNonDefaultCredentials {
+ String connectionString1 = emulator.getConnectionString("account1", "key1");
+ // the second account will not have access to the same container
+ String connectionString2 = emulator.getConnectionString("account2", "key2");
+ // }
+ BlobServiceClient blobServiceClient1 = new BlobServiceClientBuilder()
+ .connectionString(connectionString1)
+ .buildClient();
+
+ BlobContainerClient containerClient1 = blobServiceClient1.createBlobContainer("test-container");
+ BlobClient blobClient1 = containerClient1.getBlobClient("test-blob.txt");
+ blobClient1.upload(BinaryData.fromString("content"));
+ boolean existsWithAccount1 = blobClient1.exists();
+ String contentWithAccount1 = blobClient1.downloadContent().toString();
+
+ BlobServiceClient blobServiceClient2 = new BlobServiceClientBuilder()
+ .connectionString(connectionString2)
+ .buildClient();
+ BlobContainerClient containerClient2 = blobServiceClient2.createBlobContainer("test-container");
+ BlobClient blobClient2 = containerClient2.getBlobClient("test-blob.txt");
+ boolean existsWithAccount2 = blobClient2.exists();
+
+ assertThat(existsWithAccount1).isTrue();
+ assertThat(contentWithAccount1).isEqualTo("content");
+ assertThat(existsWithAccount2).isFalse();
+ }
+ }
+
+ private void testBlob(AzuriteContainer container) {
+ // createBlobClient {
+ BlobServiceClient blobServiceClient = new BlobServiceClientBuilder()
+ .connectionString(container.getConnectionString())
+ .buildClient();
+ // }
+ BlobContainerClient containerClient = blobServiceClient.createBlobContainer("test-container");
+
+ assertThat(containerClient.exists()).isTrue();
+ }
+
+ private void testQueue(AzuriteContainer container) {
+ // createQueueClient {
+ QueueServiceClient queueServiceClient = new QueueServiceClientBuilder()
+ .connectionString(container.getConnectionString())
+ .buildClient();
+ // }
+ QueueClient queueClient = queueServiceClient.createQueue("test-queue");
+
+ assertThat(queueClient.getQueueUrl()).isNotNull();
+ }
+
+ private void testTable(AzuriteContainer container) {
+ // createTableClient {
+ TableServiceClient tableServiceClient = new TableServiceClientBuilder()
+ .connectionString(container.getConnectionString())
+ .buildClient();
+ // }
+ TableClient tableClient = tableServiceClient.createTable("testtable");
+
+ assertThat(tableClient.getTableEndpoint()).isNotNull();
+ }
+}
diff --git a/modules/azure/src/test/java/org/testcontainers/azure/EventHubsEmulatorContainerTest.java b/modules/azure/src/test/java/org/testcontainers/azure/EventHubsEmulatorContainerTest.java
new file mode 100644
index 00000000000..1191c980edd
--- /dev/null
+++ b/modules/azure/src/test/java/org/testcontainers/azure/EventHubsEmulatorContainerTest.java
@@ -0,0 +1,74 @@
+package org.testcontainers.azure;
+
+import com.azure.core.util.IterableStream;
+import com.azure.messaging.eventhubs.EventData;
+import com.azure.messaging.eventhubs.EventHubClientBuilder;
+import com.azure.messaging.eventhubs.EventHubConsumerClient;
+import com.azure.messaging.eventhubs.EventHubProducerClient;
+import com.azure.messaging.eventhubs.models.EventPosition;
+import com.azure.messaging.eventhubs.models.PartitionEvent;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.Network;
+import org.testcontainers.utility.MountableFile;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.waitAtMost;
+
+class EventHubsEmulatorContainerTest {
+
+ @Test
+ public void testWithEventHubsClient() {
+ try (
+ // network {
+ Network network = Network.newNetwork();
+ // }
+ // azuriteContainer {
+ AzuriteContainer azuriteContainer = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")
+ .withNetwork(network);
+ // }
+ // emulatorContainer {
+ EventHubsEmulatorContainer emulator = new EventHubsEmulatorContainer(
+ "mcr.microsoft.com/azure-messaging/eventhubs-emulator:2.0.1"
+ )
+ .acceptLicense()
+ .withNetwork(network)
+ .withConfig(MountableFile.forClasspathResource("/eventhubs_config.json"))
+ .withAzuriteContainer(azuriteContainer);
+ // }
+ ) {
+ emulator.start();
+ // createProducerAndConsumer {
+ EventHubProducerClient producer = new EventHubClientBuilder()
+ .connectionString(emulator.getConnectionString())
+ .fullyQualifiedNamespace("emulatorNs1")
+ .eventHubName("eh1")
+ .buildProducerClient();
+ EventHubConsumerClient consumer = new EventHubClientBuilder()
+ .connectionString(emulator.getConnectionString())
+ .fullyQualifiedNamespace("emulatorNs1")
+ .eventHubName("eh1")
+ .consumerGroup("cg1")
+ .buildConsumerClient();
+ // }
+ producer.send(Collections.singletonList(new EventData("test")));
+
+ waitAtMost(Duration.ofSeconds(30))
+ .pollDelay(Duration.ofSeconds(5))
+ .untilAsserted(() -> {
+ IterableStream events = consumer.receiveFromPartition(
+ "0",
+ 1,
+ EventPosition.earliest(),
+ Duration.ofSeconds(2)
+ );
+ Optional event = events.stream().findFirst();
+ assertThat(event).isPresent();
+ assertThat(event.get().getData().getBodyAsString()).isEqualTo("test");
+ });
+ }
+ }
+}
diff --git a/modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java b/modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java
new file mode 100644
index 00000000000..83cd633c5a9
--- /dev/null
+++ b/modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java
@@ -0,0 +1,99 @@
+package org.testcontainers.azure;
+
+import com.azure.messaging.servicebus.ServiceBusClientBuilder;
+import com.azure.messaging.servicebus.ServiceBusErrorContext;
+import com.azure.messaging.servicebus.ServiceBusException;
+import com.azure.messaging.servicebus.ServiceBusMessage;
+import com.azure.messaging.servicebus.ServiceBusProcessorClient;
+import com.azure.messaging.servicebus.ServiceBusReceivedMessageContext;
+import com.azure.messaging.servicebus.ServiceBusSenderClient;
+import com.github.dockerjava.api.model.Capability;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.Network;
+import org.testcontainers.mssqlserver.MSSQLServerContainer;
+import org.testcontainers.utility.MountableFile;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+class ServiceBusEmulatorContainerTest {
+
+ @Test
+ void testWithClient() {
+ try (
+ // network {
+ Network network = Network.newNetwork();
+ // }
+ // sqlContainer {
+ MSSQLServerContainer mssqlServerContainer = new MSSQLServerContainer(
+ "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04"
+ )
+ .acceptLicense()
+ .withPassword("yourStrong(!)Password")
+ .withCreateContainerCmdModifier(cmd -> {
+ cmd.getHostConfig().withCapAdd(Capability.SYS_PTRACE);
+ })
+ .withNetwork(network);
+ // }
+ // emulatorContainer {
+ ServiceBusEmulatorContainer emulator = new ServiceBusEmulatorContainer(
+ "mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2"
+ )
+ .acceptLicense()
+ .withConfig(MountableFile.forClasspathResource("/service-bus-config.json"))
+ .withNetwork(network)
+ .withMsSqlServerContainer(mssqlServerContainer);
+ // }
+ ) {
+ emulator.start();
+ assertThat(emulator.getConnectionString()).startsWith("Endpoint=sb://");
+
+ // senderClient {
+ ServiceBusSenderClient senderClient = new ServiceBusClientBuilder()
+ .connectionString(emulator.getConnectionString())
+ .sender()
+ .queueName("queue.1")
+ .buildClient();
+ // }
+
+ await()
+ .atMost(20, TimeUnit.SECONDS)
+ .ignoreException(ServiceBusException.class)
+ .until(() -> {
+ senderClient.sendMessage(new ServiceBusMessage("Hello, Testcontainers!"));
+ return true;
+ });
+ senderClient.close();
+
+ final List received = new CopyOnWriteArrayList<>();
+ Consumer messageConsumer = m -> {
+ received.add(m.getMessage().getBody().toString());
+ m.complete();
+ };
+ Consumer errorConsumer = e -> Assertions.fail("Unexpected error: " + e);
+ // processorClient {
+ ServiceBusProcessorClient processorClient = new ServiceBusClientBuilder()
+ .connectionString(emulator.getConnectionString())
+ .processor()
+ .queueName("queue.1")
+ .processMessage(messageConsumer)
+ .processError(errorConsumer)
+ .buildProcessorClient();
+ // }
+ processorClient.start();
+
+ await()
+ .atMost(20, TimeUnit.SECONDS)
+ .untilAsserted(() -> {
+ assertThat(received).hasSize(1).containsExactlyInAnyOrder("Hello, Testcontainers!");
+ });
+ processorClient.close();
+ }
+ }
+}
diff --git a/modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java b/modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java
index 140bd64c39a..92b5814641e 100644
--- a/modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java
+++ b/modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java
@@ -4,11 +4,10 @@
import com.azure.cosmos.CosmosClientBuilder;
import com.azure.cosmos.models.CosmosContainerResponse;
import com.azure.cosmos.models.CosmosDatabaseResponse;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import org.testcontainers.utility.DockerImageName;
import java.io.FileOutputStream;
@@ -18,59 +17,60 @@
import static org.assertj.core.api.Assertions.assertThat;
-public class CosmosDBEmulatorContainerTest {
+class CosmosDBEmulatorContainerTest {
private static Properties originalSystemProperties;
- @BeforeClass
+ @BeforeAll
public static void captureOriginalSystemProperties() {
originalSystemProperties = (Properties) System.getProperties().clone();
}
- @AfterClass
+ @AfterAll
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")
- );
-
- // }
+ @TempDir
+ public Path tempFolder;
@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();
- assertThat(databaseResponse.getStatusCode()).isEqualTo(201);
- CosmosContainerResponse containerResponse = client
- .getDatabase("Azure")
- .createContainerIfNotExists("ServiceContainer", "/name")
- .block();
- assertThat(containerResponse.getStatusCode()).isEqualTo(201);
- // }
+ void testWithCosmosClient() throws Exception {
+ try (
+ // emulatorContainer {
+ CosmosDBEmulatorContainer emulator = new CosmosDBEmulatorContainer(
+ DockerImageName.parse("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest")
+ );
+ // }
+ ) {
+ emulator.start();
+ // buildAndSaveNewKeyStore {
+ Path keyStoreFile = tempFolder.resolve("azure-cosmos-emulator.keystore");
+ 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();
+ assertThat(databaseResponse.getStatusCode()).isEqualTo(201);
+ CosmosContainerResponse containerResponse = client
+ .getDatabase("Azure")
+ .createContainerIfNotExists("ServiceContainer", "/name")
+ .block();
+ assertThat(containerResponse.getStatusCode()).isEqualTo(201);
+ // }
+ }
}
}
diff --git a/modules/azure/src/test/resources/certificate.pem b/modules/azure/src/test/resources/certificate.pem
new file mode 100644
index 00000000000..30bedc29f45
--- /dev/null
+++ b/modules/azure/src/test/resources/certificate.pem
@@ -0,0 +1,23 @@
+Bag Attributes
+ friendlyName: localhost
+ localKeyID: 54 69 6D 65 20 31 37 33 34 37 32 32 33 32 31 33 31 39
+subject=CN = localhost
+issuer=CN = localhost
+-----BEGIN CERTIFICATE-----
+MIIC5zCCAc+gAwIBAgIILe7i2bhRE5cwDQYJKoZIhvcNAQEMBQAwFDESMBAGA1UE
+AxMJbG9jYWxob3N0MB4XDTI0MTIyMDE5MTg0MVoXDTQ0MTIxNTE5MTg0MVowFDES
+MBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAoqYNmLl8IiIoYrXdcoWiMQaM0lOHcV9v3A/THMremHxsR+JPm3FIOAuilFcy
+my16kuXIWHfisPxUWr9Vbf8wP/WwZutoOofJrqmruZoorQcNLCs8mQweguRmL1ju
+/lDh/9bP626vP9OjwStC4UO4f8Jga8ENoH1U+j1RsIAswYnkk3YIN6YrYv66UvtH
+IfR0ERgid2LMBIM+2KD2zw4QRyqXH7Qvo7sCsxdYYHGa6GXfza4vgvce9kJwGqn5
+wiF0Uw9XQbr/LarnR2GCy020OB81KweQJNIh27FZSRLtT+XpsjDRcC2aLBd8CRHd
+hwO2zAPI04dLbLM5XAHlEdfT7wIDAQABoz0wOzAdBgNVHQ4EFgQUPqY5isb6Q11Q
+t6dbXYHEupxADdMwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3
+DQEBDAUAA4IBAQA2katMXrTJBukiNh9yceLO/MewsxvU3KOO/O89ngfjhKXm9T8E
+RtENCmp7hLbj1Aj4PRZx3AbmUt9+tRu8fmrRXJQWgUDSHJWjDwSTBOaHcC5LDWSU
+Ex4co5Mnxvrimg7tqQg82Hw/yLH9j6gyTyh6v45QETP7IUkTZe4fg75/kPjng7Xg
+wp/QXFUx/f0dbvGRl2Fdgg0SnYFqHS3MFIjjFjv8SQlV7rZe+CD1Lxqy/Z6Fd/Fa
+33TzTuJeSAG43vdkGAvsNK/KdnxAW03T4l3pVHpNPcvsIvJUMeKOwYOjwHF/eowk
+tGrKbpUYFxUr9iKHTfu14t1oExhAsnda2Fcs
+-----END CERTIFICATE-----
diff --git a/modules/azure/src/test/resources/eventhubs_config.json b/modules/azure/src/test/resources/eventhubs_config.json
new file mode 100644
index 00000000000..554be9d7cbf
--- /dev/null
+++ b/modules/azure/src/test/resources/eventhubs_config.json
@@ -0,0 +1,24 @@
+{
+ "UserConfig": {
+ "NamespaceConfig": [
+ {
+ "Type": "EventHub",
+ "Name": "emulatorNs1",
+ "Entities": [
+ {
+ "Name": "eh1",
+ "PartitionCount": "1",
+ "ConsumerGroups": [
+ {
+ "Name": "cg1"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "LoggingConfig": {
+ "Type": "File"
+ }
+ }
+}
diff --git a/modules/azure/src/test/resources/key.pem b/modules/azure/src/test/resources/key.pem
new file mode 100644
index 00000000000..7c635f5a278
--- /dev/null
+++ b/modules/azure/src/test/resources/key.pem
@@ -0,0 +1,32 @@
+Bag Attributes
+ friendlyName: localhost
+ localKeyID: 54 69 6D 65 20 31 37 33 34 37 32 32 33 32 31 33 31 39
+Key Attributes:
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCipg2YuXwiIihi
+td1yhaIxBozSU4dxX2/cD9Mcyt6YfGxH4k+bcUg4C6KUVzKbLXqS5chYd+Kw/FRa
+v1Vt/zA/9bBm62g6h8muqau5miitBw0sKzyZDB6C5GYvWO7+UOH/1s/rbq8/06PB
+K0LhQ7h/wmBrwQ2gfVT6PVGwgCzBieSTdgg3piti/rpS+0ch9HQRGCJ3YswEgz7Y
+oPbPDhBHKpcftC+juwKzF1hgcZroZd/Nri+C9x72QnAaqfnCIXRTD1dBuv8tqudH
+YYLLTbQ4HzUrB5Ak0iHbsVlJEu1P5emyMNFwLZosF3wJEd2HA7bMA8jTh0tsszlc
+AeUR19PvAgMBAAECggEAFT8dzZKFTawqnGJncBtWyZKyeJMiwUOXSCblDADQPRkb
+x/QfNA4DQhb7AOe3G6BAP8o2dqAKg9YiasxNq5XHRsOgbIFZ1zN/vAo7/X3OzHN8
+XAW138Q+hBiz5IF4js4gB5yXAokt6WeLH6O4E9cV1dKdZ9YLIqjcnee+sRC9R/a3
+CexqLfC6b77JFbtePfq+5cn2RiK540tO/4k+F+kfJtTg78Wf2RB3A0pBAunhPSd5
+eyjiSvOZtTcvl4GdYw9nKf24I1/WUvt9FH/r1XG0CM5iwuGodbBz1iSUDaQGi5Lf
+hFWofXt7eebgsPEKciG4xTyk51p9fy9y8asY+jCbkQKBgQDG2UqJToR8G4Fk6uaO
+/XJa15TibIwQDEota0OdlXg2ZR4864fkIBv+UTbymZEM/EBuSdMM1CUBDvYHcFQX
+Aj8p1LUyKP2QYwxV/OoPfJ5fBqxqONNR1fLFg7xCxnf9kSvsni2WFneQUrTDl8+7
+qnHm4IKPkAxZ4Orxl5qIBmlpGQKBgQDRZUL3cHIVLLg/aZACpo6SYDYg2bztXmz4
+lRk9j17q1uS83Umzd2lPFmSt/Nr85EKraxXZ/lYPKrP/r1pf1/35eXOWqmYBWgo/
+Hh7OzL12bhvv9UWEY/TvW+wNJNtXlJSjEFRN4tjoG2amYumyhwMO1lIulplUWvtw
+ymm8hDjeRwKBgCq7n60KVqZlMtWBNbMc/GpRUgmm0iLQwVApcQp4iLEH4gutgjKg
+Q+PPiENyhR2JSD9rVhO3s4warvzCQw/+x5wxvg7diEBzSL9h7tsNKOu6/2qEc8Vu
+eRHBUb/37ulrPUlIZPuQMHmvjHFMOrRV2MyJCwXXKxBVqafpsKfy2MxhAoGBAIHH
+Cswk6u/ouYDDwjeCVxatfp65lHhhb5RZhD09IIzYBwhu9gC+34veyyNydZ8LMa7g
+PbjQAzJ/OvQbEB4a1hPKjDMzBOmNjpAz8NAm4L4H3FTKZP16nhHDnPdAgpkzQzQV
+KMrk755bbTFuWH0HZIPLnT+2ou0/PltXeFUYdc59AoGBAIGfWgSOiw7aXbSQZFrO
+4S0v3VTwTaiGDVS4pkNRLlhEJUhy8+gbLv/zYDmFmGtqVhXTb/nd6DOdylp+W/HS
+8xNWBMWdlX/hVdSK7M0TdJvAaCaMidlquf5qZ2tGNNDeTUN1qbRH26pm8vdNZ3gr
+Y/WWJGo0iEmwyB8RcFhvNmuJ
+-----END PRIVATE KEY-----
diff --git a/modules/azure/src/test/resources/keystore.pfx b/modules/azure/src/test/resources/keystore.pfx
new file mode 100644
index 00000000000..3fd2975d3e8
Binary files /dev/null and b/modules/azure/src/test/resources/keystore.pfx differ
diff --git a/modules/azure/src/test/resources/service-bus-config.json b/modules/azure/src/test/resources/service-bus-config.json
new file mode 100644
index 00000000000..18ac2e69c7b
--- /dev/null
+++ b/modules/azure/src/test/resources/service-bus-config.json
@@ -0,0 +1,29 @@
+{
+ "UserConfig": {
+ "Namespaces": [
+ {
+ "Name": "sbemulatorns",
+ "Queues": [
+ {
+ "Name": "queue.1",
+ "Properties": {
+ "DeadLetteringOnMessageExpiration": false,
+ "DefaultMessageTimeToLive": "PT1H",
+ "DuplicateDetectionHistoryTimeWindow": "PT20S",
+ "ForwardDeadLetteredMessagesTo": "",
+ "ForwardTo": "",
+ "LockDuration": "PT1M",
+ "MaxDeliveryCount": 3,
+ "RequiresDuplicateDetection": false,
+ "RequiresSession": false
+ }
+ }
+ ],
+ "Topics": []
+ }
+ ],
+ "Logging": {
+ "Type": "File"
+ }
+ }
+}
diff --git a/modules/cassandra/build.gradle b/modules/cassandra/build.gradle
index 7f9ab53c71b..b0bea00226e 100644
--- a/modules/cassandra/build.gradle
+++ b/modules/cassandra/build.gradle
@@ -1,4 +1,4 @@
-description = "TestContainers :: Cassandra"
+description = "Testcontainers :: Cassandra"
configurations.all {
resolutionStrategy {
@@ -7,9 +7,8 @@ configurations.all {
}
dependencies {
- api project(":database-commons")
+ api project(":testcontainers-database-commons")
api "com.datastax.cassandra:cassandra-driver-core:3.10.0"
- testImplementation 'com.datastax.oss:java-driver-core:4.15.0'
- testImplementation 'org.assertj:assertj-core:3.23.1'
+ testImplementation 'com.datastax.oss:java-driver-core:4.17.0'
}
diff --git a/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraContainer.java b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraContainer.java
new file mode 100644
index 00000000000..3fb8554f137
--- /dev/null
+++ b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraContainer.java
@@ -0,0 +1,211 @@
+package org.testcontainers.cassandra;
+
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import org.apache.commons.lang3.StringUtils;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.ext.ScriptUtils;
+import org.testcontainers.ext.ScriptUtils.ScriptLoadException;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+import java.net.InetSocketAddress;
+import java.util.Optional;
+
+/**
+ * Testcontainers implementation for Apache Cassandra.
+ *
+ * Supported image: {@code cassandra}
+ *
+ * Exposed ports: 9042
+ */
+public class CassandraContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("cassandra");
+
+ private static final Integer CQL_PORT = 9042;
+
+ private static final String DEFAULT_LOCAL_DATACENTER = "datacenter1";
+
+ private static final String DEFAULT_INIT_SCRIPT_FILENAME = "init.cql";
+
+ private static final String CONTAINER_CONFIG_LOCATION = "/etc/cassandra";
+
+ private static final String USERNAME = "cassandra";
+
+ private static final String PASSWORD = "cassandra";
+
+ private String configLocation;
+
+ private String initScriptPath;
+
+ private String clientCertFile;
+
+ private String clientKeyFile;
+
+ public CassandraContainer(String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ public CassandraContainer(DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+
+ addExposedPort(CQL_PORT);
+
+ withEnv("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch");
+ withEnv("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0");
+ withEnv("HEAP_NEWSIZE", "128M");
+ withEnv("MAX_HEAP_SIZE", "1024M");
+ withEnv("CASSANDRA_ENDPOINT_SNITCH", "GossipingPropertyFileSnitch");
+ withEnv("CASSANDRA_DC", DEFAULT_LOCAL_DATACENTER);
+
+ // Use the CassandraQueryWaitStrategy by default to avoid potential issues when the authentication is enabled.
+ waitingFor(new CassandraQueryWaitStrategy());
+ }
+
+ @Override
+ protected void configure() {
+ // Map (effectively replace) directory in Docker with the content of resourceLocation if resource location is
+ // not null.
+ Optional
+ .ofNullable(configLocation)
+ .map(MountableFile::forClasspathResource)
+ .ifPresent(mountableFile -> withCopyFileToContainer(mountableFile, CONTAINER_CONFIG_LOCATION));
+
+ // If a secure connection is required by Cassandra configuration, copy the user certificate and key to a
+ // dedicated location and define a cqlshrc file with the appropriate SSL configuration.
+ // See: https://docs.datastax.com/en/cassandra-oss/3.x/cassandra/configuration/secureCqlshSSL.html
+ if (isSslRequired()) {
+ withCopyFileToContainer(MountableFile.forClasspathResource(clientCertFile), "ssl/user_cert.pem");
+ withCopyFileToContainer(MountableFile.forClasspathResource(clientKeyFile), "ssl/user_key.pem");
+ withCopyFileToContainer(MountableFile.forClasspathResource("cqlshrc"), "/root/.cassandra/cqlshrc");
+ }
+ }
+
+ @Override
+ protected void containerIsStarted(InspectContainerResponse containerInfo) {
+ runInitScriptIfRequired();
+ }
+
+ /**
+ * Load init script content and apply it to the database if initScriptPath is set
+ */
+ private void runInitScriptIfRequired() {
+ if (this.initScriptPath != null) {
+ try {
+ final MountableFile originalInitScript = MountableFile.forClasspathResource(this.initScriptPath);
+ // The init script is executed as is by the cqlsh command, so copy it into the container. The name
+ // of the script is generic since it's not important to keep the original name.
+ copyFileToContainer(originalInitScript, DEFAULT_INIT_SCRIPT_FILENAME);
+ new CassandraDatabaseDelegate(this).execute(null, DEFAULT_INIT_SCRIPT_FILENAME, -1, false, false);
+ } catch (IllegalArgumentException e) {
+ // MountableFile.forClasspathResource will throw an IllegalArgumentException if the resource cannot
+ // be found.
+ logger().warn("Could not load classpath init script: {}", this.initScriptPath);
+ throw new ScriptLoadException(
+ "Could not load classpath init script: " + this.initScriptPath + ". Resource not found.",
+ e
+ );
+ } catch (ScriptUtils.ScriptStatementFailedException e) {
+ logger().error("Error while executing init script: {}", this.initScriptPath, e);
+ throw new ScriptUtils.UncategorizedScriptException(
+ "Error while executing init script: " + this.initScriptPath,
+ e
+ );
+ }
+ }
+ }
+
+ /**
+ * Initialize Cassandra with the custom overridden Cassandra configuration
+ *
+ * Be aware, that Docker effectively replaces all /etc/cassandra content with the content of config location, so if
+ * Cassandra.yaml in configLocation is absent or corrupted, then Cassandra just won't launch.
+ *
+ * @param configLocation relative classpath with the directory that contains cassandra.yaml and other configuration
+ * files
+ * @return The updated {@link CassandraContainer}.
+ */
+ public CassandraContainer withConfigurationOverride(String configLocation) {
+ this.configLocation = configLocation;
+ return self();
+ }
+
+ /**
+ * Initialize Cassandra with init CQL script
+ *
+ * CQL script will be applied after container is started (see using WaitStrategy).
+ *
+ *
+ * @param initScriptPath relative classpath resource
+ * @return The updated {@link CassandraContainer}.
+ */
+ public CassandraContainer withInitScript(String initScriptPath) {
+ this.initScriptPath = initScriptPath;
+ return self();
+ }
+
+ /**
+ * Configure secured connection (TLS) when required by the Cassandra configuration
+ * (i.e. cassandra.yaml file contains the property {@code client_encryption_options.optional} with value
+ * {@code false}).
+ *
+ * @param clientCertFile The client certificate required to execute CQL scripts.
+ * @param clientKeyFile The client key required to execute CQL scripts.
+ * @return The updated {@link CassandraContainer}.
+ */
+ public CassandraContainer withSsl(String clientCertFile, String clientKeyFile) {
+ this.clientCertFile = clientCertFile;
+ this.clientKeyFile = clientKeyFile;
+ return self();
+ }
+
+ /**
+ * @return Whether a secure connection is required between the client and the Cassandra server.
+ */
+ boolean isSslRequired() {
+ return StringUtils.isNoneBlank(this.clientCertFile, this.clientKeyFile);
+ }
+
+ /**
+ * Get username
+ *
+ * By default, Cassandra has authenticator: AllowAllAuthenticator in cassandra.yaml
+ * If username and password need to be used, then authenticator should be set as PasswordAuthenticator
+ * (through custom Cassandra configuration) and through CQL with default cassandra-cassandra credentials
+ * user management should be modified
+ */
+ public String getUsername() {
+ return USERNAME;
+ }
+
+ /**
+ * Get password
+ *
+ * By default, Cassandra has authenticator: AllowAllAuthenticator in cassandra.yaml
+ * If username and password need to be used, then authenticator should be set as PasswordAuthenticator
+ * (through custom Cassandra configuration) and through CQL with default cassandra-cassandra credentials
+ * user management should be modified
+ */
+ public String getPassword() {
+ return PASSWORD;
+ }
+
+ /**
+ * Retrieve an {@link InetSocketAddress} for connecting to the Cassandra container via the driver.
+ *
+ * @return A InetSocketAddress representation of this Cassandra container's host and port.
+ */
+ public InetSocketAddress getContactPoint() {
+ return new InetSocketAddress(getHost(), getMappedPort(CQL_PORT));
+ }
+
+ /**
+ * Retrieve the Local Datacenter for connecting to the Cassandra container via the driver.
+ *
+ * @return The configured local Datacenter name.
+ */
+ public String getLocalDatacenter() {
+ return getEnvMap().getOrDefault("CASSANDRA_DC", DEFAULT_LOCAL_DATACENTER);
+ }
+}
diff --git a/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraDatabaseDelegate.java b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraDatabaseDelegate.java
new file mode 100644
index 00000000000..4867b8423e9
--- /dev/null
+++ b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraDatabaseDelegate.java
@@ -0,0 +1,98 @@
+package org.testcontainers.cassandra;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.ContainerState;
+import org.testcontainers.containers.ExecConfig;
+import org.testcontainers.delegate.AbstractDatabaseDelegate;
+import org.testcontainers.ext.ScriptUtils.ScriptStatementFailedException;
+
+import java.io.IOException;
+
+/**
+ * Cassandra database delegate
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class CassandraDatabaseDelegate extends AbstractDatabaseDelegate {
+
+ private final ContainerState container;
+
+ @Override
+ protected Void createNewConnection() {
+ // Return null here, because we run scripts using cqlsh command directly in the container.
+ // So, we don't use connection object in the execute() method.
+ return null;
+ }
+
+ public void execute(
+ String statement,
+ String scriptPath,
+ int lineNumber,
+ boolean continueOnError,
+ boolean ignoreFailedDrops,
+ boolean silentErrorLogs
+ ) {
+ try {
+ // Use cqlsh command directly inside the container to execute statements
+ // See documentation here: https://cassandra.apache.org/doc/stable/cassandra/tools/cqlsh.html
+ String[] cqlshCommand = new String[] { "cqlsh" };
+
+ if (this.container instanceof CassandraContainer) {
+ CassandraContainer cassandraContainer = (CassandraContainer) this.container;
+ String username = cassandraContainer.getUsername();
+ String password = cassandraContainer.getPassword();
+ if (cassandraContainer.isSslRequired()) {
+ cqlshCommand = ArrayUtils.add(cqlshCommand, "--ssl");
+ }
+ cqlshCommand = ArrayUtils.addAll(cqlshCommand, "-u", username, "-p", password);
+ }
+
+ // If no statement specified, directly execute the script specified into scriptPath (using -f argument),
+ // otherwise execute the given statement (using -e argument).
+ String executeArg = "-e";
+ String executeArgValue = statement;
+ if (StringUtils.isBlank(statement)) {
+ executeArg = "-f";
+ executeArgValue = scriptPath;
+ }
+ cqlshCommand = ArrayUtils.addAll(cqlshCommand, executeArg, executeArgValue);
+
+ Container.ExecResult result =
+ this.container.execInContainer(ExecConfig.builder().command(cqlshCommand).build());
+ if (result.getExitCode() == 0) {
+ if (StringUtils.isBlank(statement)) {
+ log.info("CQL script {} successfully executed", scriptPath);
+ } else {
+ log.info("CQL statement {} was applied", statement);
+ }
+ } else {
+ if (!silentErrorLogs) {
+ log.error("CQL script execution failed with error: \n{}", result.getStderr());
+ }
+ throw new ScriptStatementFailedException(statement, lineNumber, scriptPath);
+ }
+ } catch (IOException | InterruptedException e) {
+ throw new ScriptStatementFailedException(statement, lineNumber, scriptPath, e);
+ }
+ }
+
+ @Override
+ public void execute(
+ String statement,
+ String scriptPath,
+ int lineNumber,
+ boolean continueOnError,
+ boolean ignoreFailedDrops
+ ) {
+ this.execute(statement, scriptPath, lineNumber, continueOnError, ignoreFailedDrops, false);
+ }
+
+ @Override
+ protected void closeConnectionQuietly(Void session) {
+ // Nothing to do here, because we run scripts using cqlsh command directly in the container.
+ }
+}
diff --git a/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java
new file mode 100644
index 00000000000..19fdcd7f9e1
--- /dev/null
+++ b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java
@@ -0,0 +1,57 @@
+package org.testcontainers.cassandra;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.rnorth.ducttape.TimeoutException;
+import org.testcontainers.containers.ContainerLaunchException;
+import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
+import org.testcontainers.delegate.DatabaseDelegate;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess;
+
+/**
+ * Waits until Cassandra returns its version
+ */
+@Slf4j
+public class CassandraQueryWaitStrategy extends AbstractWaitStrategy {
+
+ private static final String SELECT_VERSION_QUERY = "SELECT release_version FROM system.local";
+
+ private static final String TIMEOUT_ERROR = "Timed out waiting for Cassandra to be accessible for query execution";
+
+ @Override
+ protected void waitUntilReady() {
+ // execute select version query until success or timeout
+ try {
+ retryUntilSuccess(
+ (int) startupTimeout.getSeconds(),
+ TimeUnit.SECONDS,
+ () -> {
+ getRateLimiter()
+ .doWhenReady(() -> {
+ try (DatabaseDelegate databaseDelegate = getDatabaseDelegate()) {
+ log.info("Checking connection is ready...");
+ ((CassandraDatabaseDelegate) databaseDelegate).execute(
+ SELECT_VERSION_QUERY,
+ StringUtils.EMPTY,
+ 1,
+ false,
+ false,
+ true
+ );
+ }
+ });
+ return true;
+ }
+ );
+ } catch (TimeoutException e) {
+ throw new ContainerLaunchException(TIMEOUT_ERROR);
+ }
+ }
+
+ private DatabaseDelegate getDatabaseDelegate() {
+ return new CassandraDatabaseDelegate(waitStrategyTarget);
+ }
+}
diff --git a/modules/cassandra/src/main/java/org/testcontainers/containers/CassandraContainer.java b/modules/cassandra/src/main/java/org/testcontainers/containers/CassandraContainer.java
index a12ac03f18d..fcaada177ce 100644
--- a/modules/cassandra/src/main/java/org/testcontainers/containers/CassandraContainer.java
+++ b/modules/cassandra/src/main/java/org/testcontainers/containers/CassandraContainer.java
@@ -19,12 +19,15 @@
import javax.script.ScriptException;
/**
- * Cassandra container
+ * Testcontainers implementation for Apache Cassandra.
+ *
+ * Supported image: {@code cassandra}
+ *
+ * Exposed ports: 9042
*
- * Supports 2.x and 3.x Cassandra versions
- *
- * @author Eugeny Karpov
+ * @deprecated use {@link org.testcontainers.cassandra.CassandraContainer} instead.
*/
+@Deprecated
public class CassandraContainer> extends GenericContainer {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("cassandra");
@@ -225,7 +228,7 @@ public static Cluster getCluster(ContainerState containerState) {
/**
* Retrieve an {@link InetSocketAddress} for connecting to the Cassandra container via the driver.
*
- * @return A InetSocketAddrss representation of this Cassandra container's host and port.
+ * @return A InetSocketAddress representation of this Cassandra container's host and port.
*/
public InetSocketAddress getContactPoint() {
return new InetSocketAddress(getHost(), getMappedPort(CQL_PORT));
diff --git a/modules/cassandra/src/main/java/org/testcontainers/containers/delegate/CassandraDatabaseDelegate.java b/modules/cassandra/src/main/java/org/testcontainers/containers/delegate/CassandraDatabaseDelegate.java
index 8bc72e7f154..7b7739490ea 100644
--- a/modules/cassandra/src/main/java/org/testcontainers/containers/delegate/CassandraDatabaseDelegate.java
+++ b/modules/cassandra/src/main/java/org/testcontainers/containers/delegate/CassandraDatabaseDelegate.java
@@ -14,10 +14,11 @@
/**
* Cassandra database delegate
*
- * @author Eugeny Karpov
+ * @deprecated use {@link org.testcontainers.cassandra.CassandraDatabaseDelegate} instead.
*/
@Slf4j
@RequiredArgsConstructor
+@Deprecated
public class CassandraDatabaseDelegate extends AbstractDatabaseDelegate {
private final ContainerState container;
diff --git a/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java b/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java
index aa2466cf7c4..9694711de6e 100644
--- a/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java
+++ b/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java
@@ -13,8 +13,9 @@
/**
* Waits until Cassandra returns its version
*
- * @author Eugeny Karpov
+ * @deprecated use {@link org.testcontainers.cassandra.CassandraQueryWaitStrategy} instead.
*/
+@Deprecated
public class CassandraQueryWaitStrategy extends AbstractWaitStrategy {
private static final String SELECT_VERSION_QUERY = "SELECT release_version FROM system.local";
diff --git a/modules/cassandra/src/main/resources/cqlshrc b/modules/cassandra/src/main/resources/cqlshrc
new file mode 100644
index 00000000000..02e001098af
--- /dev/null
+++ b/modules/cassandra/src/main/resources/cqlshrc
@@ -0,0 +1,7 @@
+[ssl]
+certfile = ssl/user_cert.pem
+usercert = ssl/user_cert.pem
+userkey = ssl/user_key.pem
+
+[connection]
+factory = cqlshlib.ssl.ssl_transport_factory
\ No newline at end of file
diff --git a/modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java b/modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java
new file mode 100644
index 00000000000..49243317a40
--- /dev/null
+++ b/modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java
@@ -0,0 +1,272 @@
+package org.testcontainers.cassandra;
+
+import com.datastax.oss.driver.api.core.CqlSession;
+import com.datastax.oss.driver.api.core.CqlSessionBuilder;
+import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
+import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
+import com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBuilder;
+import com.datastax.oss.driver.api.core.context.DriverContext;
+import com.datastax.oss.driver.api.core.cql.ResultSet;
+import com.datastax.oss.driver.api.core.cql.Row;
+import com.datastax.oss.driver.api.core.session.ProgrammaticArguments;
+import com.datastax.oss.driver.internal.core.context.DefaultDriverContext;
+import com.datastax.oss.driver.internal.core.ssl.DefaultSslEngineFactory;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.ContainerLaunchException;
+import org.testcontainers.utility.DockerImageName;
+
+import java.net.URL;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.fail;
+
+class CassandraContainerTest {
+
+ private static final String CASSANDRA_IMAGE = "cassandra:3.11.15";
+
+ private static final String TEST_CLUSTER_NAME_IN_CONF = "Test Cluster Integration Test";
+
+ private static final String BASIC_QUERY = "SELECT release_version FROM system.local";
+
+ @Test
+ void testSimple() {
+ try ( // container-definition {
+ CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE)
+ // }
+ ) {
+ cassandraContainer.start();
+ ResultSet resultSet = performQuery(cassandraContainer, BASIC_QUERY);
+ assertThat(resultSet.wasApplied()).as("Query was applied").isTrue();
+ assertThat(resultSet.one().getString(0)).as("Result set has release_version").isNotNull();
+ }
+ }
+
+ @Test
+ void testSpecificVersion() {
+ String cassandraVersion = "3.0.15";
+ try (
+ CassandraContainer cassandraContainer = new CassandraContainer(
+ DockerImageName.parse("cassandra").withTag(cassandraVersion)
+ )
+ ) {
+ cassandraContainer.start();
+ ResultSet resultSet = performQuery(cassandraContainer, BASIC_QUERY);
+ assertThat(resultSet.wasApplied()).as("Query was applied").isTrue();
+ assertThat(resultSet.one().getString(0)).as("Cassandra has right version").isEqualTo(cassandraVersion);
+ }
+ }
+
+ @Test
+ void testConfigurationOverride() {
+ try (
+ CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE)
+ .withConfigurationOverride("cassandra-test-configuration-example")
+ ) {
+ cassandraContainer.start();
+ ResultSet resultSet = performQuery(cassandraContainer, "SELECT cluster_name FROM system.local");
+ assertThat(resultSet.wasApplied()).as("Query was applied").isTrue();
+ assertThat(resultSet.one().getString(0))
+ .as("Cassandra configuration is overridden")
+ .isEqualTo(TEST_CLUSTER_NAME_IN_CONF);
+ }
+ }
+
+ @Test
+ public void testWithSslClientConfig() {
+ /*
+ Commands executed to generate certificates in 'cassandra-ssl-configuration' directory:
+ keytool -genkey -keyalg RSA -validity 36500 -alias localhost -keystore keystore.p12 -storepass cassandra \
+ -keypass cassandra -dname "CN=localhost, OU=Testcontainers, O=Testcontainers, L=None, C=None"
+ keytool -export -alias localhost -file cassandra.cer -keystore keystore.p12
+ keytool -import -v -trustcacerts -alias localhost -file cassandra.cer -keystore truststore.p12
+
+ Commands executed to generate the client certificate and key in 'client-ssl' directory:
+ keytool -importkeystore -srckeystore keystore.p12 -destkeystore test_node.p12 -deststoretype PKCS12 \
+ -srcstorepass cassandra -deststorepass cassandra
+ openssl pkcs12 -in test_node.p12 -nokeys -out cassandra.cer.pem -passin pass:cassandra
+ openssl pkcs12 -in test_node.p12 -nodes -nocerts -out cassandra.key.pem -passin pass:cassandra
+
+ Reference:
+ https://docs.datastax.com/en/cassandra-oss/3.x/cassandra/configuration/secureSSLCertificates.html
+ https://docs.datastax.com/en/cassandra-oss/3.x/cassandra/configuration/secureCqlshSSL.html
+ */
+ try (
+ // with-ssl-config {
+ CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE)
+ .withConfigurationOverride("cassandra-ssl-configuration")
+ .withSsl("client-ssl/cassandra.cer.pem", "client-ssl/cassandra.key.pem")
+ // }
+ ) {
+ cassandraContainer.start();
+ try {
+ ResultSet resultSet = performQueryWithSslClientConfig(
+ cassandraContainer,
+ "SELECT cluster_name FROM system.local"
+ );
+ assertThat(resultSet.wasApplied()).as("Query was applied").isTrue();
+ assertThat(resultSet.one().getString(0))
+ .as("Cassandra configuration is configured with secured connection")
+ .isEqualTo(TEST_CLUSTER_NAME_IN_CONF);
+ } catch (Exception e) {
+ fail(e);
+ }
+ }
+ }
+
+ @Test
+ public void testSimpleSslCqlsh() {
+ try (
+ CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE)
+ .withConfigurationOverride("cassandra-ssl-configuration")
+ .withSsl("client-ssl/cassandra.cer.pem", "client-ssl/cassandra.key.pem")
+ ) {
+ cassandraContainer.start();
+
+ Container.ExecResult execResult = cassandraContainer.execInContainer(
+ "cqlsh",
+ "--ssl",
+ "-e",
+ "SELECT * FROM system_schema.keyspaces;"
+ );
+ assertThat(execResult.getStdout()).contains("keyspace_name");
+ } catch (Exception e) {
+ fail(e);
+ }
+ }
+
+ @Test
+ void testEmptyConfigurationOverride() {
+ try (
+ CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE)
+ .withConfigurationOverride("cassandra-empty-configuration")
+ ) {
+ assertThatThrownBy(cassandraContainer::start).isInstanceOf(ContainerLaunchException.class);
+ }
+ }
+
+ @Test
+ void testInitScript() {
+ try (
+ CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE)
+ .withInitScript("initial.cql")
+ ) {
+ cassandraContainer.start();
+ testInitScript(cassandraContainer, false);
+ }
+ }
+
+ @Test
+ void testNonexistentInitScript() {
+ try (
+ CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE)
+ .withInitScript("unknown_script.cql")
+ ) {
+ assertThatThrownBy(cassandraContainer::start).isInstanceOf(ContainerLaunchException.class);
+ }
+ }
+
+ @Test
+ void testInitScriptWithRequiredAuthentication() {
+ try (
+ // init-with-auth {
+ CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE)
+ .withConfigurationOverride("cassandra-auth-required-configuration")
+ .withInitScript("initial.cql")
+ // }
+ ) {
+ cassandraContainer.start();
+ testInitScript(cassandraContainer, true);
+ }
+ }
+
+ @Test
+ void testInitScriptWithError() {
+ try (
+ CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE)
+ .withInitScript("initial-with-error.cql")
+ ) {
+ assertThatThrownBy(cassandraContainer::start).isInstanceOf(ContainerLaunchException.class);
+ }
+ }
+
+ @Test
+ void testInitScriptWithLegacyCassandra() {
+ try (
+ CassandraContainer cassandraContainer = new CassandraContainer("cassandra:2.2.11")
+ .withInitScript("initial.cql")
+ ) {
+ cassandraContainer.start();
+ testInitScript(cassandraContainer, false);
+ }
+ }
+
+ private void testInitScript(CassandraContainer cassandraContainer, boolean withCredentials) {
+ String query = "SELECT * FROM keySpaceTest.catalog_category";
+ ResultSet resultSet;
+
+ if (withCredentials) {
+ resultSet = performQueryWithAuth(cassandraContainer, query);
+ } else {
+ resultSet = performQuery(cassandraContainer, query);
+ }
+
+ assertThat(resultSet.wasApplied()).as("Query was applied").isTrue();
+ Row row = resultSet.one();
+ assertThat(row.getLong(0)).as("Inserted row is in expected state").isEqualTo(1);
+ assertThat(row.getString(1)).as("Inserted row is in expected state").isEqualTo("test_category");
+ }
+
+ private ResultSet performQuery(CassandraContainer cassandraContainer, String cql) {
+ // cql-session {
+ final CqlSession cqlSession = CqlSession
+ .builder()
+ .addContactPoint(cassandraContainer.getContactPoint())
+ .withLocalDatacenter(cassandraContainer.getLocalDatacenter())
+ .build();
+ // }
+ return performQuery(cqlSession, cql);
+ }
+
+ private ResultSet performQueryWithAuth(CassandraContainer cassandraContainer, String cql) {
+ final CqlSession cqlSession = CqlSession
+ .builder()
+ .addContactPoint(cassandraContainer.getContactPoint())
+ .withLocalDatacenter(cassandraContainer.getLocalDatacenter())
+ .withAuthCredentials(cassandraContainer.getUsername(), cassandraContainer.getPassword())
+ .build();
+ return performQuery(cqlSession, cql);
+ }
+
+ private ResultSet performQueryWithSslClientConfig(CassandraContainer cassandraContainer, String cql) {
+ final ProgrammaticDriverConfigLoaderBuilder driverConfigLoaderBuilder = DriverConfigLoader.programmaticBuilder();
+ driverConfigLoaderBuilder.withBoolean(DefaultDriverOption.SSL_HOSTNAME_VALIDATION, false);
+ final URL trustStoreUrl =
+ this.getClass().getClassLoader().getResource("cassandra-ssl-configuration/truststore.p12");
+ driverConfigLoaderBuilder.withString(DefaultDriverOption.SSL_TRUSTSTORE_PATH, trustStoreUrl.getFile());
+ driverConfigLoaderBuilder.withString(DefaultDriverOption.SSL_TRUSTSTORE_PASSWORD, "cassandra");
+ final URL keyStoreUrl =
+ this.getClass().getClassLoader().getResource("cassandra-ssl-configuration/keystore.p12");
+ driverConfigLoaderBuilder.withString(DefaultDriverOption.SSL_KEYSTORE_PATH, keyStoreUrl.getFile());
+ driverConfigLoaderBuilder.withString(DefaultDriverOption.SSL_KEYSTORE_PASSWORD, "cassandra");
+ final DriverContext driverContext = new DefaultDriverContext(
+ driverConfigLoaderBuilder.build(),
+ ProgrammaticArguments.builder().build()
+ );
+
+ final CqlSessionBuilder sessionBuilder = CqlSession.builder();
+ final CqlSession cqlSession = sessionBuilder
+ .addContactPoint(cassandraContainer.getContactPoint())
+ .withLocalDatacenter(cassandraContainer.getLocalDatacenter())
+ .withSslEngineFactory(new DefaultSslEngineFactory(driverContext))
+ .build();
+ return performQuery(cqlSession, cql);
+ }
+
+ private ResultSet performQuery(CqlSession session, String cql) {
+ final ResultSet rs = session.execute(cql);
+ session.close();
+ return rs;
+ }
+}
diff --git a/modules/cassandra/src/test/java/org/testcontainers/cassandra/CompatibleCassandraImageTest.java b/modules/cassandra/src/test/java/org/testcontainers/cassandra/CompatibleCassandraImageTest.java
new file mode 100644
index 00000000000..15d8e209911
--- /dev/null
+++ b/modules/cassandra/src/test/java/org/testcontainers/cassandra/CompatibleCassandraImageTest.java
@@ -0,0 +1,44 @@
+package org.testcontainers.cassandra;
+
+import com.datastax.oss.driver.api.core.CqlIdentifier;
+import com.datastax.oss.driver.api.core.CqlSession;
+import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CompatibleCassandraImageTest {
+
+ public static String[] params() {
+ return new String[] { "cassandra:3.11.2", "cassandra:4.1.1", "cassandra:5" };
+ }
+
+ @ParameterizedTest
+ @MethodSource("params")
+ void testCassandraGetContactPoint(String imageName) {
+ try (CassandraContainer cassandra = new CassandraContainer(imageName)) {
+ cassandra.start();
+ assertCassandraFunctionality(cassandra);
+ }
+ }
+
+ private void assertCassandraFunctionality(CassandraContainer cassandra) {
+ try (
+ CqlSession session = CqlSession
+ .builder()
+ .addContactPoint(cassandra.getContactPoint())
+ .withLocalDatacenter(cassandra.getLocalDatacenter())
+ .build()
+ ) {
+ session.execute(
+ "CREATE KEYSPACE IF NOT EXISTS test WITH replication = \n" +
+ "{'class':'SimpleStrategy','replication_factor':'1'};"
+ );
+
+ KeyspaceMetadata keyspace = session.getMetadata().getKeyspaces().get(CqlIdentifier.fromCql("test"));
+
+ assertThat(keyspace).as("test keyspace created").isNotNull();
+ }
+ }
+}
diff --git a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraContainerTest.java b/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraContainerTest.java
index d8f21fc3068..2d534bccc81 100644
--- a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraContainerTest.java
+++ b/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraContainerTest.java
@@ -5,17 +5,15 @@
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import lombok.extern.slf4j.Slf4j;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.testcontainers.containers.wait.CassandraQueryWaitStrategy;
import org.testcontainers.utility.DockerImageName;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
-/**
- * @author Eugeny Karpov
- */
@Slf4j
-public class CassandraContainerTest {
+class CassandraContainerTest {
private static final DockerImageName CASSANDRA_IMAGE = DockerImageName.parse("cassandra:3.11.2");
@@ -24,7 +22,7 @@ public class CassandraContainerTest {
private static final String BASIC_QUERY = "SELECT release_version FROM system.local";
@Test
- public void testSimple() {
+ void testSimple() {
try (CassandraContainer> cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE)) {
cassandraContainer.start();
ResultSet resultSet = performQuery(cassandraContainer, BASIC_QUERY);
@@ -34,7 +32,7 @@ public void testSimple() {
}
@Test
- public void testSpecificVersion() {
+ void testSpecificVersion() {
String cassandraVersion = "3.0.15";
try (
CassandraContainer> cassandraContainer = new CassandraContainer<>(
@@ -49,7 +47,7 @@ public void testSpecificVersion() {
}
@Test
- public void testConfigurationOverride() {
+ void testConfigurationOverride() {
try (
CassandraContainer> cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE)
.withConfigurationOverride("cassandra-test-configuration-example")
@@ -63,18 +61,18 @@ public void testConfigurationOverride() {
}
}
- @Test(expected = ContainerLaunchException.class)
- public void testEmptyConfigurationOverride() {
+ @Test
+ void testEmptyConfigurationOverride() {
try (
CassandraContainer> cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE)
.withConfigurationOverride("cassandra-empty-configuration")
) {
- cassandraContainer.start();
+ assertThatThrownBy(cassandraContainer::start).isInstanceOf(ContainerLaunchException.class);
}
}
@Test
- public void testInitScript() {
+ void testInitScript() {
try (
CassandraContainer> cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE)
.withInitScript("initial.cql")
@@ -85,7 +83,7 @@ public void testInitScript() {
}
@Test
- public void testInitScriptWithLegacyCassandra() {
+ void testInitScriptWithLegacyCassandra() {
try (
CassandraContainer> cassandraContainer = new CassandraContainer<>(
DockerImageName.parse("cassandra:2.2.11")
@@ -99,7 +97,7 @@ public void testInitScriptWithLegacyCassandra() {
@SuppressWarnings("deprecation") // Using deprecated constructor for verification of backwards compatibility
@Test
- public void testCassandraQueryWaitStrategy() {
+ void testCassandraQueryWaitStrategy() {
try (
CassandraContainer> cassandraContainer = new CassandraContainer<>()
.waitingFor(new CassandraQueryWaitStrategy())
@@ -112,7 +110,7 @@ public void testCassandraQueryWaitStrategy() {
@SuppressWarnings("deprecation") // Using deprecated constructor for verification of backwards compatibility
@Test
- public void testCassandraGetCluster() {
+ void testCassandraGetCluster() {
try (CassandraContainer> cassandraContainer = new CassandraContainer<>()) {
cassandraContainer.start();
ResultSet resultSet = performQuery(cassandraContainer.getCluster(), BASIC_QUERY);
diff --git a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver3Test.java b/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver3Test.java
deleted file mode 100644
index 502f2ca8e67..00000000000
--- a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver3Test.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.testcontainers.containers;
-
-import com.datastax.oss.driver.api.core.CqlIdentifier;
-import com.datastax.oss.driver.api.core.CqlSession;
-import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;
-import org.junit.Rule;
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class CassandraDriver3Test {
-
- @Rule
- public CassandraContainer> cassandra = new CassandraContainer<>("cassandra:3.11.2");
-
- @Test
- public void testCassandraGetContactPoint() {
- try (
- // cassandra {
- CqlSession session = CqlSession
- .builder()
- .addContactPoint(this.cassandra.getContactPoint())
- .withLocalDatacenter(this.cassandra.getLocalDatacenter())
- .build()
- // }
- ) {
- session.execute(
- "CREATE KEYSPACE IF NOT EXISTS test WITH replication = \n" +
- "{'class':'SimpleStrategy','replication_factor':'1'};"
- );
-
- KeyspaceMetadata keyspace = session.getMetadata().getKeyspaces().get(CqlIdentifier.fromCql("test"));
-
- assertThat(keyspace).as("keyspace created").isNotNull();
- }
- }
-}
diff --git a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver4Test.java b/modules/cassandra/src/test/java/org/testcontainers/containers/CompatibleCassandraImageTest.java
similarity index 50%
rename from modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver4Test.java
rename to modules/cassandra/src/test/java/org/testcontainers/containers/CompatibleCassandraImageTest.java
index a4fb8c87b8e..aec27566beb 100644
--- a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver4Test.java
+++ b/modules/cassandra/src/test/java/org/testcontainers/containers/CompatibleCassandraImageTest.java
@@ -3,23 +3,32 @@
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
import static org.assertj.core.api.Assertions.assertThat;
-public class CassandraDriver4Test {
+public class CompatibleCassandraImageTest {
- @Rule
- public CassandraContainer> cassandra = new CassandraContainer<>("cassandra:3.11.2");
+ public static String[] params() {
+ return new String[] { "cassandra:3.11.2", "cassandra:4.1.1" };
+ }
+
+ @ParameterizedTest
+ @MethodSource("params")
+ void testCassandraGetContactPoint(String imageName) {
+ try (CassandraContainer> cassandra = new CassandraContainer<>(imageName)) {
+ cassandra.start();
+ assertCassandraFunctionality(cassandra);
+ }
+ }
- @Test
- public void testCassandraGetContactPoint() {
+ private void assertCassandraFunctionality(CassandraContainer> cassandra) {
try (
CqlSession session = CqlSession
.builder()
- .addContactPoint(this.cassandra.getContactPoint())
- .withLocalDatacenter(this.cassandra.getLocalDatacenter())
+ .addContactPoint(cassandra.getContactPoint())
+ .withLocalDatacenter(cassandra.getLocalDatacenter())
.build()
) {
session.execute(
diff --git a/modules/cassandra/src/test/resources/cassandra-auth-required-configuration/cassandra.yaml b/modules/cassandra/src/test/resources/cassandra-auth-required-configuration/cassandra.yaml
new file mode 100644
index 00000000000..7425881b8fe
--- /dev/null
+++ b/modules/cassandra/src/test/resources/cassandra-auth-required-configuration/cassandra.yaml
@@ -0,0 +1,1233 @@
+# Cassandra storage config YAML
+
+# NOTE:
+# See http://wiki.apache.org/cassandra/StorageConfiguration for
+# full explanations of configuration directives
+# /NOTE
+
+# The name of the cluster. This is mainly used to prevent machines in
+# one logical cluster from joining another.
+cluster_name: 'Test Cluster Integration Test'
+
+# This defines the number of tokens randomly assigned to this node on the ring
+# The more tokens, relative to other nodes, the larger the proportion of data
+# that this node will store. You probably want all nodes to have the same number
+# of tokens assuming they have equal hardware capability.
+#
+# If you leave this unspecified, Cassandra will use the default of 1 token for legacy compatibility,
+# and will use the initial_token as described below.
+#
+# Specifying initial_token will override this setting on the node's initial start,
+# on subsequent starts, this setting will apply even if initial token is set.
+#
+# If you already have a cluster with 1 token per node, and wish to migrate to
+# multiple tokens per node, see http://wiki.apache.org/cassandra/Operations
+num_tokens: 256
+
+# Triggers automatic allocation of num_tokens tokens for this node. The allocation
+# algorithm attempts to choose tokens in a way that optimizes replicated load over
+# the nodes in the datacenter for the replication strategy used by the specified
+# keyspace.
+#
+# The load assigned to each node will be close to proportional to its number of
+# vnodes.
+#
+# Only supported with the Murmur3Partitioner.
+# allocate_tokens_for_keyspace: KEYSPACE
+
+# initial_token allows you to specify tokens manually. While you can use it with
+# vnodes (num_tokens > 1, above) -- in which case you should provide a
+# comma-separated list -- it's primarily used when adding nodes to legacy clusters
+# that do not have vnodes enabled.
+# initial_token:
+
+# See http://wiki.apache.org/cassandra/HintedHandoff
+# May either be "true" or "false" to enable globally
+hinted_handoff_enabled: true
+
+# When hinted_handoff_enabled is true, a black list of data centers that will not
+# perform hinted handoff
+# hinted_handoff_disabled_datacenters:
+# - DC1
+# - DC2
+
+# this defines the maximum amount of time a dead host will have hints
+# generated. After it has been dead this long, new hints for it will not be
+# created until it has been seen alive and gone down again.
+max_hint_window_in_ms: 10800000 # 3 hours
+
+# Maximum throttle in KBs per second, per delivery thread. This will be
+# reduced proportionally to the number of nodes in the cluster. (If there
+# are two nodes in the cluster, each delivery thread will use the maximum
+# rate; if there are three, each will throttle to half of the maximum,
+# since we expect two nodes to be delivering hints simultaneously.)
+hinted_handoff_throttle_in_kb: 1024
+
+# Number of threads with which to deliver hints;
+# Consider increasing this number when you have multi-dc deployments, since
+# cross-dc handoff tends to be slower
+max_hints_delivery_threads: 2
+
+# Directory where Cassandra should store hints.
+# If not set, the default directory is $CASSANDRA_HOME/data/hints.
+# hints_directory: /var/lib/cassandra/hints
+
+# How often hints should be flushed from the internal buffers to disk.
+# Will *not* trigger fsync.
+hints_flush_period_in_ms: 10000
+
+# Maximum size for a single hints file, in megabytes.
+max_hints_file_size_in_mb: 128
+
+# Compression to apply to the hint files. If omitted, hints files
+# will be written uncompressed. LZ4, Snappy, and Deflate compressors
+# are supported.
+#hints_compression:
+# - class_name: LZ4Compressor
+# parameters:
+# -
+
+# Maximum throttle in KBs per second, total. This will be
+# reduced proportionally to the number of nodes in the cluster.
+batchlog_replay_throttle_in_kb: 1024
+
+# Authentication backend, implementing IAuthenticator; used to identify users
+# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator,
+# PasswordAuthenticator}.
+#
+# - AllowAllAuthenticator performs no checks - set it to disable authentication.
+# - PasswordAuthenticator relies on username/password pairs to authenticate
+# users. It keeps usernames and hashed passwords in system_auth.roles table.
+# Please increase system_auth keyspace replication factor if you use this authenticator.
+# If using PasswordAuthenticator, CassandraRoleManager must also be used (see below)
+authenticator: PasswordAuthenticator
+
+# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions
+# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer,
+# CassandraAuthorizer}.
+#
+# - AllowAllAuthorizer allows any action to any user - set it to disable authorization.
+# - CassandraAuthorizer stores permissions in system_auth.role_permissions table. Please
+# increase system_auth keyspace replication factor if you use this authorizer.
+authorizer: AllowAllAuthorizer
+
+# Part of the Authentication & Authorization backend, implementing IRoleManager; used
+# to maintain grants and memberships between roles.
+# Out of the box, Cassandra provides org.apache.cassandra.auth.CassandraRoleManager,
+# which stores role information in the system_auth keyspace. Most functions of the
+# IRoleManager require an authenticated login, so unless the configured IAuthenticator
+# actually implements authentication, most of this functionality will be unavailable.
+#
+# - CassandraRoleManager stores role data in the system_auth keyspace. Please
+# increase system_auth keyspace replication factor if you use this role manager.
+role_manager: CassandraRoleManager
+
+# Validity period for roles cache (fetching granted roles can be an expensive
+# operation depending on the role manager, CassandraRoleManager is one example)
+# Granted roles are cached for authenticated sessions in AuthenticatedUser and
+# after the period specified here, become eligible for (async) reload.
+# Defaults to 2000, set to 0 to disable caching entirely.
+# Will be disabled automatically for AllowAllAuthenticator.
+roles_validity_in_ms: 2000
+
+# Refresh interval for roles cache (if enabled).
+# After this interval, cache entries become eligible for refresh. Upon next
+# access, an async reload is scheduled and the old value returned until it
+# completes. If roles_validity_in_ms is non-zero, then this must be
+# also.
+# Defaults to the same value as roles_validity_in_ms.
+# roles_update_interval_in_ms: 2000
+
+# Validity period for permissions cache (fetching permissions can be an
+# expensive operation depending on the authorizer, CassandraAuthorizer is
+# one example). Defaults to 2000, set to 0 to disable.
+# Will be disabled automatically for AllowAllAuthorizer.
+permissions_validity_in_ms: 2000
+
+# Refresh interval for permissions cache (if enabled).
+# After this interval, cache entries become eligible for refresh. Upon next
+# access, an async reload is scheduled and the old value returned until it
+# completes. If permissions_validity_in_ms is non-zero, then this must be
+# also.
+# Defaults to the same value as permissions_validity_in_ms.
+# permissions_update_interval_in_ms: 2000
+
+# Validity period for credentials cache. This cache is tightly coupled to
+# the provided PasswordAuthenticator implementation of IAuthenticator. If
+# another IAuthenticator implementation is configured, this cache will not
+# be automatically used and so the following settings will have no effect.
+# Please note, credentials are cached in their encrypted form, so while
+# activating this cache may reduce the number of queries made to the
+# underlying table, it may not bring a significant reduction in the
+# latency of individual authentication attempts.
+# Defaults to 2000, set to 0 to disable credentials caching.
+credentials_validity_in_ms: 2000
+
+# Refresh interval for credentials cache (if enabled).
+# After this interval, cache entries become eligible for refresh. Upon next
+# access, an async reload is scheduled and the old value returned until it
+# completes. If credentials_validity_in_ms is non-zero, then this must be
+# also.
+# Defaults to the same value as credentials_validity_in_ms.
+# credentials_update_interval_in_ms: 2000
+
+# The partitioner is responsible for distributing groups of rows (by
+# partition key) across nodes in the cluster. You should leave this
+# alone for new clusters. The partitioner can NOT be changed without
+# reloading all data, so when upgrading you should set this to the
+# same partitioner you were already using.
+#
+# Besides Murmur3Partitioner, partitioners included for backwards
+# compatibility include RandomPartitioner, ByteOrderedPartitioner, and
+# OrderPreservingPartitioner.
+#
+partitioner: org.apache.cassandra.dht.Murmur3Partitioner
+
+# Directories where Cassandra should store data on disk. Cassandra
+# will spread data evenly across them, subject to the granularity of
+# the configured compaction strategy.
+# If not set, the default directory is $CASSANDRA_HOME/data/data.
+data_file_directories:
+ - /var/lib/cassandra/data
+
+# commit log. when running on magnetic HDD, this should be a
+# separate spindle than the data directories.
+# If not set, the default directory is $CASSANDRA_HOME/data/commitlog.
+commitlog_directory: /var/lib/cassandra/commitlog
+
+# Enable / disable CDC functionality on a per-node basis. This modifies the logic used
+# for write path allocation rejection (standard: never reject. cdc: reject Mutation
+# containing a CDC-enabled table if at space limit in cdc_raw_directory).
+cdc_enabled: false
+
+# CommitLogSegments are moved to this directory on flush if cdc_enabled: true and the
+# segment contains mutations for a CDC-enabled table. This should be placed on a
+# separate spindle than the data directories. If not set, the default directory is
+# $CASSANDRA_HOME/data/cdc_raw.
+# cdc_raw_directory: /var/lib/cassandra/cdc_raw
+
+# Policy for data disk failures:
+#
+# die
+# shut down gossip and client transports and kill the JVM for any fs errors or
+# single-sstable errors, so the node can be replaced.
+#
+# stop_paranoid
+# shut down gossip and client transports even for single-sstable errors,
+# kill the JVM for errors during startup.
+#
+# stop
+# shut down gossip and client transports, leaving the node effectively dead, but
+# can still be inspected via JMX, kill the JVM for errors during startup.
+#
+# best_effort
+# stop using the failed disk and respond to requests based on
+# remaining available sstables. This means you WILL see obsolete
+# data at CL.ONE!
+#
+# ignore
+# ignore fatal errors and let requests fail, as in pre-1.2 Cassandra
+disk_failure_policy: stop
+
+# Policy for commit disk failures:
+#
+# die
+# shut down gossip and Thrift and kill the JVM, so the node can be replaced.
+#
+# stop
+# shut down gossip and Thrift, leaving the node effectively dead, but
+# can still be inspected via JMX.
+#
+# stop_commit
+# shutdown the commit log, letting writes collect but
+# continuing to service reads, as in pre-2.0.5 Cassandra
+#
+# ignore
+# ignore fatal errors and let the batches fail
+commit_failure_policy: stop
+
+# Maximum size of the native protocol prepared statement cache
+#
+# Valid values are either "auto" (omitting the value) or a value greater 0.
+#
+# Note that specifying a too large value will result in long running GCs and possibly
+# out-of-memory errors. Keep the value at a small fraction of the heap.
+#
+# If you constantly see "prepared statements discarded in the last minute because
+# cache limit reached" messages, the first step is to investigate the root cause
+# of these messages and check whether prepared statements are used correctly -
+# i.e. use bind markers for variable parts.
+#
+# Do only change the default value, if you really have more prepared statements than
+# fit in the cache. In most cases it is not necessary to change this value.
+# Constantly re-preparing statements is a performance penalty.
+#
+# Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater
+prepared_statements_cache_size_mb:
+
+# Maximum size of the Thrift prepared statement cache
+#
+# If you do not use Thrift at all, it is safe to leave this value at "auto".
+#
+# See description of 'prepared_statements_cache_size_mb' above for more information.
+#
+# Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater
+thrift_prepared_statements_cache_size_mb:
+
+# Maximum size of the key cache in memory.
+#
+# Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the
+# minimum, sometimes more. The key cache is fairly tiny for the amount of
+# time it saves, so it's worthwhile to use it at large numbers.
+# The row cache saves even more time, but must contain the entire row,
+# so it is extremely space-intensive. It's best to only use the
+# row cache if you have hot rows or static rows.
+#
+# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup.
+#
+# Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache.
+key_cache_size_in_mb:
+
+# Duration in seconds after which Cassandra should
+# save the key cache. Caches are saved to saved_caches_directory as
+# specified in this configuration file.
+#
+# Saved caches greatly improve cold-start speeds, and is relatively cheap in
+# terms of I/O for the key cache. Row cache saving is much more expensive and
+# has limited use.
+#
+# Default is 14400 or 4 hours.
+key_cache_save_period: 14400
+
+# Number of keys from the key cache to save
+# Disabled by default, meaning all keys are going to be saved
+# key_cache_keys_to_save: 100
+
+# Row cache implementation class name. Available implementations:
+#
+# org.apache.cassandra.cache.OHCProvider
+# Fully off-heap row cache implementation (default).
+#
+# org.apache.cassandra.cache.SerializingCacheProvider
+# This is the row cache implementation available
+# in previous releases of Cassandra.
+# row_cache_class_name: org.apache.cassandra.cache.OHCProvider
+
+# Maximum size of the row cache in memory.
+# Please note that OHC cache implementation requires some additional off-heap memory to manage
+# the map structures and some in-flight memory during operations before/after cache entries can be
+# accounted against the cache capacity. This overhead is usually small compared to the whole capacity.
+# Do not specify more memory that the system can afford in the worst usual situation and leave some
+# headroom for OS block level cache. Do never allow your system to swap.
+#
+# Default value is 0, to disable row caching.
+row_cache_size_in_mb: 0
+
+# Duration in seconds after which Cassandra should save the row cache.
+# Caches are saved to saved_caches_directory as specified in this configuration file.
+#
+# Saved caches greatly improve cold-start speeds, and is relatively cheap in
+# terms of I/O for the key cache. Row cache saving is much more expensive and
+# has limited use.
+#
+# Default is 0 to disable saving the row cache.
+row_cache_save_period: 0
+
+# Number of keys from the row cache to save.
+# Specify 0 (which is the default), meaning all keys are going to be saved
+# row_cache_keys_to_save: 100
+
+# Maximum size of the counter cache in memory.
+#
+# Counter cache helps to reduce counter locks' contention for hot counter cells.
+# In case of RF = 1 a counter cache hit will cause Cassandra to skip the read before
+# write entirely. With RF > 1 a counter cache hit will still help to reduce the duration
+# of the lock hold, helping with hot counter cell updates, but will not allow skipping
+# the read entirely. Only the local (clock, count) tuple of a counter cell is kept
+# in memory, not the whole counter, so it's relatively cheap.
+#
+# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup.
+#
+# Default value is empty to make it "auto" (min(2.5% of Heap (in MB), 50MB)). Set to 0 to disable counter cache.
+# NOTE: if you perform counter deletes and rely on low gcgs, you should disable the counter cache.
+counter_cache_size_in_mb:
+
+# Duration in seconds after which Cassandra should
+# save the counter cache (keys only). Caches are saved to saved_caches_directory as
+# specified in this configuration file.
+#
+# Default is 7200 or 2 hours.
+counter_cache_save_period: 7200
+
+# Number of keys from the counter cache to save
+# Disabled by default, meaning all keys are going to be saved
+# counter_cache_keys_to_save: 100
+
+# saved caches
+# If not set, the default directory is $CASSANDRA_HOME/data/saved_caches.
+saved_caches_directory: /var/lib/cassandra/saved_caches
+
+# commitlog_sync may be either "periodic" or "batch."
+#
+# When in batch mode, Cassandra won't ack writes until the commit log
+# has been fsynced to disk. It will wait
+# commitlog_sync_batch_window_in_ms milliseconds between fsyncs.
+# This window should be kept short because the writer threads will
+# be unable to do extra work while waiting. (You may need to increase
+# concurrent_writes for the same reason.)
+#
+# commitlog_sync: batch
+# commitlog_sync_batch_window_in_ms: 2
+#
+# the other option is "periodic" where writes may be acked immediately
+# and the CommitLog is simply synced every commitlog_sync_period_in_ms
+# milliseconds.
+commitlog_sync: periodic
+commitlog_sync_period_in_ms: 10000
+
+# The size of the individual commitlog file segments. A commitlog
+# segment may be archived, deleted, or recycled once all the data
+# in it (potentially from each columnfamily in the system) has been
+# flushed to sstables.
+#
+# The default size is 32, which is almost always fine, but if you are
+# archiving commitlog segments (see commitlog_archiving.properties),
+# then you probably want a finer granularity of archiving; 8 or 16 MB
+# is reasonable.
+# Max mutation size is also configurable via max_mutation_size_in_kb setting in
+# cassandra.yaml. The default is half the size commitlog_segment_size_in_mb * 1024.
+# This should be positive and less than 2048.
+#
+# NOTE: If max_mutation_size_in_kb is set explicitly then commitlog_segment_size_in_mb must
+# be set to at least twice the size of max_mutation_size_in_kb / 1024
+#
+commitlog_segment_size_in_mb: 32
+
+# Compression to apply to the commit log. If omitted, the commit log
+# will be written uncompressed. LZ4, Snappy, and Deflate compressors
+# are supported.
+# commitlog_compression:
+# - class_name: LZ4Compressor
+# parameters:
+# -
+
+# any class that implements the SeedProvider interface and has a
+# constructor that takes a Map of parameters will do.
+seed_provider:
+ # Addresses of hosts that are deemed contact points.
+ # Cassandra nodes use this list of hosts to find each other and learn
+ # the topology of the ring. You must change this if you are running
+ # multiple nodes!
+ - class_name: org.apache.cassandra.locator.SimpleSeedProvider
+ parameters:
+ # seeds is actually a comma-delimited list of addresses.
+ # Ex: ",,"
+ - seeds: "172.17.0.2"
+
+# For workloads with more data than can fit in memory, Cassandra's
+# bottleneck will be reads that need to fetch data from
+# disk. "concurrent_reads" should be set to (16 * number_of_drives) in
+# order to allow the operations to enqueue low enough in the stack
+# that the OS and drives can reorder them. Same applies to
+# "concurrent_counter_writes", since counter writes read the current
+# values before incrementing and writing them back.
+#
+# On the other hand, since writes are almost never IO bound, the ideal
+# number of "concurrent_writes" is dependent on the number of cores in
+# your system; (8 * number_of_cores) is a good rule of thumb.
+concurrent_reads: 32
+concurrent_writes: 32
+concurrent_counter_writes: 32
+
+# For materialized view writes, as there is a read involved, so this should
+# be limited by the less of concurrent reads or concurrent writes.
+concurrent_materialized_view_writes: 32
+
+# Maximum memory to use for sstable chunk cache and buffer pooling.
+# 32MB of this are reserved for pooling buffers, the rest is used as a
+# cache that holds uncompressed sstable chunks.
+# Defaults to the smaller of 1/4 of heap or 512MB. This pool is allocated off-heap,
+# so is in addition to the memory allocated for heap. The cache also has on-heap
+# overhead which is roughly 128 bytes per chunk (i.e. 0.2% of the reserved size
+# if the default 64k chunk size is used).
+# Memory is only allocated when needed.
+# file_cache_size_in_mb: 512
+
+# Flag indicating whether to allocate on or off heap when the sstable buffer
+# pool is exhausted, that is when it has exceeded the maximum memory
+# file_cache_size_in_mb, beyond which it will not cache buffers but allocate on request.
+
+# buffer_pool_use_heap_if_exhausted: true
+
+# The strategy for optimizing disk read
+# Possible values are:
+# ssd (for solid state disks, the default)
+# spinning (for spinning disks)
+# disk_optimization_strategy: ssd
+
+# Total permitted memory to use for memtables. Cassandra will stop
+# accepting writes when the limit is exceeded until a flush completes,
+# and will trigger a flush based on memtable_cleanup_threshold
+# If omitted, Cassandra will set both to 1/4 the size of the heap.
+# memtable_heap_space_in_mb: 2048
+# memtable_offheap_space_in_mb: 2048
+
+# memtable_cleanup_threshold is deprecated. The default calculation
+# is the only reasonable choice. See the comments on memtable_flush_writers
+# for more information.
+#
+# Ratio of occupied non-flushing memtable size to total permitted size
+# that will trigger a flush of the largest memtable. Larger mct will
+# mean larger flushes and hence less compaction, but also less concurrent
+# flush activity which can make it difficult to keep your disks fed
+# under heavy write load.
+#
+# memtable_cleanup_threshold defaults to 1 / (memtable_flush_writers + 1)
+# memtable_cleanup_threshold: 0.11
+
+# Specify the way Cassandra allocates and manages memtable memory.
+# Options are:
+#
+# heap_buffers
+# on heap nio buffers
+#
+# offheap_buffers
+# off heap (direct) nio buffers
+#
+# offheap_objects
+# off heap objects
+memtable_allocation_type: heap_buffers
+
+# Total space to use for commit logs on disk.
+#
+# If space gets above this value, Cassandra will flush every dirty CF
+# in the oldest segment and remove it. So a small total commitlog space
+# will tend to cause more flush activity on less-active columnfamilies.
+#
+# The default value is the smaller of 8192, and 1/4 of the total space
+# of the commitlog volume.
+#
+# commitlog_total_space_in_mb: 8192
+
+# This sets the number of memtable flush writer threads per disk
+# as well as the total number of memtables that can be flushed concurrently.
+# These are generally a combination of compute and IO bound.
+#
+# Memtable flushing is more CPU efficient than memtable ingest and a single thread
+# can keep up with the ingest rate of a whole server on a single fast disk
+# until it temporarily becomes IO bound under contention typically with compaction.
+# At that point you need multiple flush threads. At some point in the future
+# it may become CPU bound all the time.
+#
+# You can tell if flushing is falling behind using the MemtablePool.BlockedOnAllocation
+# metric which should be 0, but will be non-zero if threads are blocked waiting on flushing
+# to free memory.
+#
+# memtable_flush_writers defaults to two for a single data directory.
+# This means that two memtables can be flushed concurrently to the single data directory.
+# If you have multiple data directories the default is one memtable flushing at a time
+# but the flush will use a thread per data directory so you will get two or more writers.
+#
+# Two is generally enough to flush on a fast disk [array] mounted as a single data directory.
+# Adding more flush writers will result in smaller more frequent flushes that introduce more
+# compaction overhead.
+#
+# There is a direct tradeoff between number of memtables that can be flushed concurrently
+# and flush size and frequency. More is not better you just need enough flush writers
+# to never stall waiting for flushing to free memory.
+#
+#memtable_flush_writers: 2
+
+# Total space to use for change-data-capture logs on disk.
+#
+# If space gets above this value, Cassandra will throw WriteTimeoutException
+# on Mutations including tables with CDC enabled. A CDCCompactor is responsible
+# for parsing the raw CDC logs and deleting them when parsing is completed.
+#
+# The default value is the min of 4096 mb and 1/8th of the total space
+# of the drive where cdc_raw_directory resides.
+# cdc_total_space_in_mb: 4096
+
+# When we hit our cdc_raw limit and the CDCCompactor is either running behind
+# or experiencing backpressure, we check at the following interval to see if any
+# new space for cdc-tracked tables has been made available. Default to 250ms
+# cdc_free_space_check_interval_ms: 250
+
+# A fixed memory pool size in MB for SSTable index summaries. If left
+# empty, this will default to 5% of the heap size. If the memory usage of
+# all index summaries exceeds this limit, SSTables with low read rates will
+# shrink their index summaries in order to meet this limit. However, this
+# is a best-effort process. In extreme conditions Cassandra may need to use
+# more than this amount of memory.
+index_summary_capacity_in_mb:
+
+# How frequently index summaries should be resampled. This is done
+# periodically to redistribute memory from the fixed-size pool to sstables
+# proportional their recent read rates. Setting to -1 will disable this
+# process, leaving existing index summaries at their current sampling level.
+index_summary_resize_interval_in_minutes: 60
+
+# Whether to, when doing sequential writing, fsync() at intervals in
+# order to force the operating system to flush the dirty
+# buffers. Enable this to avoid sudden dirty buffer flushing from
+# impacting read latencies. Almost always a good idea on SSDs; not
+# necessarily on platters.
+trickle_fsync: false
+trickle_fsync_interval_in_kb: 10240
+
+# TCP port, for commands and data
+# For security reasons, you should not expose this port to the internet. Firewall it if needed.
+storage_port: 7000
+
+# SSL port, for encrypted communication. Unused unless enabled in
+# encryption_options
+# For security reasons, you should not expose this port to the internet. Firewall it if needed.
+ssl_storage_port: 7001
+
+# Address or interface to bind to and tell other Cassandra nodes to connect to.
+# You _must_ change this if you want multiple nodes to be able to communicate!
+#
+# Set listen_address OR listen_interface, not both.
+#
+# Leaving it blank leaves it up to InetAddress.getLocalHost(). This
+# will always do the Right Thing _if_ the node is properly configured
+# (hostname, name resolution, etc), and the Right Thing is to use the
+# address associated with the hostname (it might not be).
+#
+# Setting listen_address to 0.0.0.0 is always wrong.
+#
+listen_address: 172.17.0.2
+
+# Set listen_address OR listen_interface, not both. Interfaces must correspond
+# to a single address, IP aliasing is not supported.
+# listen_interface: eth0
+
+# If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address
+# you can specify which should be chosen using listen_interface_prefer_ipv6. If false the first ipv4
+# address will be used. If true the first ipv6 address will be used. Defaults to false preferring
+# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6.
+# listen_interface_prefer_ipv6: false
+
+# Address to broadcast to other Cassandra nodes
+# Leaving this blank will set it to the same value as listen_address
+broadcast_address: 172.17.0.2
+
+# When using multiple physical network interfaces, set this
+# to true to listen on broadcast_address in addition to
+# the listen_address, allowing nodes to communicate in both
+# interfaces.
+# Ignore this property if the network configuration automatically
+# routes between the public and private networks such as EC2.
+# listen_on_broadcast_address: false
+
+# Internode authentication backend, implementing IInternodeAuthenticator;
+# used to allow/disallow connections from peer nodes.
+# internode_authenticator: org.apache.cassandra.auth.AllowAllInternodeAuthenticator
+
+# Whether to start the native transport server.
+# Please note that the address on which the native transport is bound is the
+# same as the rpc_address. The port however is different and specified below.
+start_native_transport: true
+# port for the CQL native transport to listen for clients on
+# For security reasons, you should not expose this port to the internet. Firewall it if needed.
+native_transport_port: 9042
+# Enabling native transport encryption in client_encryption_options allows you to either use
+# encryption for the standard port or to use a dedicated, additional port along with the unencrypted
+# standard native_transport_port.
+# Enabling client encryption and keeping native_transport_port_ssl disabled will use encryption
+# for native_transport_port. Setting native_transport_port_ssl to a different value
+# from native_transport_port will use encryption for native_transport_port_ssl while
+# keeping native_transport_port unencrypted.
+# native_transport_port_ssl: 9142
+# The maximum threads for handling requests when the native transport is used.
+# This is similar to rpc_max_threads though the default differs slightly (and
+# there is no native_transport_min_threads, idle threads will always be stopped
+# after 30 seconds).
+# native_transport_max_threads: 128
+#
+# The maximum size of allowed frame. Frame (requests) larger than this will
+# be rejected as invalid. The default is 256MB. If you're changing this parameter,
+# you may want to adjust max_value_size_in_mb accordingly. This should be positive and less than 2048.
+# native_transport_max_frame_size_in_mb: 256
+
+# The maximum number of concurrent client connections.
+# The default is -1, which means unlimited.
+# native_transport_max_concurrent_connections: -1
+
+# The maximum number of concurrent client connections per source ip.
+# The default is -1, which means unlimited.
+# native_transport_max_concurrent_connections_per_ip: -1
+
+# Whether to start the thrift rpc server.
+start_rpc: false
+
+# The address or interface to bind the Thrift RPC service and native transport
+# server to.
+#
+# Set rpc_address OR rpc_interface, not both.
+#
+# Leaving rpc_address blank has the same effect as on listen_address
+# (i.e. it will be based on the configured hostname of the node).
+#
+# Note that unlike listen_address, you can specify 0.0.0.0, but you must also
+# set broadcast_rpc_address to a value other than 0.0.0.0.
+#
+# For security reasons, you should not expose this port to the internet. Firewall it if needed.
+rpc_address: 0.0.0.0
+
+# Set rpc_address OR rpc_interface, not both. Interfaces must correspond
+# to a single address, IP aliasing is not supported.
+# rpc_interface: eth1
+
+# If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address
+# you can specify which should be chosen using rpc_interface_prefer_ipv6. If false the first ipv4
+# address will be used. If true the first ipv6 address will be used. Defaults to false preferring
+# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6.
+# rpc_interface_prefer_ipv6: false
+
+# port for Thrift to listen for clients on
+rpc_port: 9160
+
+# RPC address to broadcast to drivers and other Cassandra nodes. This cannot
+# be set to 0.0.0.0. If left blank, this will be set to the value of
+# rpc_address. If rpc_address is set to 0.0.0.0, broadcast_rpc_address must
+# be set.
+broadcast_rpc_address: 172.17.0.2
+
+# enable or disable keepalive on rpc/native connections
+rpc_keepalive: true
+
+# Cassandra provides two out-of-the-box options for the RPC Server:
+#
+# sync
+# One thread per thrift connection. For a very large number of clients, memory
+# will be your limiting factor. On a 64 bit JVM, 180KB is the minimum stack size
+# per thread, and that will correspond to your use of virtual memory (but physical memory
+# may be limited depending on use of stack space).
+#
+# hsha
+# Stands for "half synchronous, half asynchronous." All thrift clients are handled
+# asynchronously using a small number of threads that does not vary with the amount
+# of thrift clients (and thus scales well to many clients). The rpc requests are still
+# synchronous (one thread per active request). If hsha is selected then it is essential
+# that rpc_max_threads is changed from the default value of unlimited.
+#
+# The default is sync because on Windows hsha is about 30% slower. On Linux,
+# sync/hsha performance is about the same, with hsha of course using less memory.
+#
+# Alternatively, can provide your own RPC server by providing the fully-qualified class name
+# of an o.a.c.t.TServerFactory that can create an instance of it.
+rpc_server_type: sync
+
+# Uncomment rpc_min|max_thread to set request pool size limits.
+#
+# Regardless of your choice of RPC server (see above), the number of maximum requests in the
+# RPC thread pool dictates how many concurrent requests are possible (but if you are using the sync
+# RPC server, it also dictates the number of clients that can be connected at all).
+#
+# The default is unlimited and thus provides no protection against clients overwhelming the server. You are
+# encouraged to set a maximum that makes sense for you in production, but do keep in mind that
+# rpc_max_threads represents the maximum number of client requests this server may execute concurrently.
+#
+# rpc_min_threads: 16
+# rpc_max_threads: 2048
+
+# uncomment to set socket buffer sizes on rpc connections
+# rpc_send_buff_size_in_bytes:
+# rpc_recv_buff_size_in_bytes:
+
+# Uncomment to set socket buffer size for internode communication
+# Note that when setting this, the buffer size is limited by net.core.wmem_max
+# and when not setting it it is defined by net.ipv4.tcp_wmem
+# See also:
+# /proc/sys/net/core/wmem_max
+# /proc/sys/net/core/rmem_max
+# /proc/sys/net/ipv4/tcp_wmem
+# /proc/sys/net/ipv4/tcp_wmem
+# and 'man tcp'
+# internode_send_buff_size_in_bytes:
+
+# Uncomment to set socket buffer size for internode communication
+# Note that when setting this, the buffer size is limited by net.core.wmem_max
+# and when not setting it it is defined by net.ipv4.tcp_wmem
+# internode_recv_buff_size_in_bytes:
+
+# Frame size for thrift (maximum message length).
+thrift_framed_transport_size_in_mb: 15
+
+# Set to true to have Cassandra create a hard link to each sstable
+# flushed or streamed locally in a backups/ subdirectory of the
+# keyspace data. Removing these links is the operator's
+# responsibility.
+incremental_backups: false
+
+# Whether or not to take a snapshot before each compaction. Be
+# careful using this option, since Cassandra won't clean up the
+# snapshots for you. Mostly useful if you're paranoid when there
+# is a data format change.
+snapshot_before_compaction: false
+
+# Whether or not a snapshot is taken of the data before keyspace truncation
+# or dropping of column families. The STRONGLY advised default of true
+# should be used to provide data safety. If you set this flag to false, you will
+# lose data on truncation or drop.
+auto_snapshot: true
+
+# Granularity of the collation index of rows within a partition.
+# Increase if your rows are large, or if you have a very large
+# number of rows per partition. The competing goals are these:
+#
+# - a smaller granularity means more index entries are generated
+# and looking up rows within the partition by collation column
+# is faster
+# - but, Cassandra will keep the collation index in memory for hot
+# rows (as part of the key cache), so a larger granularity means
+# you can cache more hot rows
+column_index_size_in_kb: 64
+
+# Per sstable indexed key cache entries (the collation index in memory
+# mentioned above) exceeding this size will not be held on heap.
+# This means that only partition information is held on heap and the
+# index entries are read from disk.
+#
+# Note that this size refers to the size of the
+# serialized index information and not the size of the partition.
+column_index_cache_size_in_kb: 2
+
+# Number of simultaneous compactions to allow, NOT including
+# validation "compactions" for anti-entropy repair. Simultaneous
+# compactions can help preserve read performance in a mixed read/write
+# workload, by mitigating the tendency of small sstables to accumulate
+# during a single long running compactions. The default is usually
+# fine and if you experience problems with compaction running too
+# slowly or too fast, you should look at
+# compaction_throughput_mb_per_sec first.
+#
+# concurrent_compactors defaults to the smaller of (number of disks,
+# number of cores), with a minimum of 2 and a maximum of 8.
+#
+# If your data directories are backed by SSD, you should increase this
+# to the number of cores.
+#concurrent_compactors: 1
+
+# Throttles compaction to the given total throughput across the entire
+# system. The faster you insert data, the faster you need to compact in
+# order to keep the sstable count down, but in general, setting this to
+# 16 to 32 times the rate you are inserting data is more than sufficient.
+# Setting this to 0 disables throttling. Note that this account for all types
+# of compaction, including validation compaction.
+compaction_throughput_mb_per_sec: 16
+
+# When compacting, the replacement sstable(s) can be opened before they
+# are completely written, and used in place of the prior sstables for
+# any range that has been written. This helps to smoothly transfer reads
+# between the sstables, reducing page cache churn and keeping hot rows hot
+sstable_preemptive_open_interval_in_mb: 50
+
+# Throttles all outbound streaming file transfers on this node to the
+# given total throughput in Mbps. This is necessary because Cassandra does
+# mostly sequential IO when streaming data during bootstrap or repair, which
+# can lead to saturating the network connection and degrading rpc performance.
+# When unset, the default is 200 Mbps or 25 MB/s.
+# stream_throughput_outbound_megabits_per_sec: 200
+
+# Throttles all streaming file transfer between the datacenters,
+# this setting allows users to throttle inter dc stream throughput in addition
+# to throttling all network stream traffic as configured with
+# stream_throughput_outbound_megabits_per_sec
+# When unset, the default is 200 Mbps or 25 MB/s
+# inter_dc_stream_throughput_outbound_megabits_per_sec: 200
+
+# How long the coordinator should wait for read operations to complete
+read_request_timeout_in_ms: 5000
+# How long the coordinator should wait for seq or index scans to complete
+range_request_timeout_in_ms: 10000
+# How long the coordinator should wait for writes to complete
+write_request_timeout_in_ms: 2000
+# How long the coordinator should wait for counter writes to complete
+counter_write_request_timeout_in_ms: 5000
+# How long a coordinator should continue to retry a CAS operation
+# that contends with other proposals for the same row
+cas_contention_timeout_in_ms: 1000
+# How long the coordinator should wait for truncates to complete
+# (This can be much longer, because unless auto_snapshot is disabled
+# we need to flush first so we can snapshot before removing the data.)
+truncate_request_timeout_in_ms: 60000
+# The default timeout for other, miscellaneous operations
+request_timeout_in_ms: 10000
+
+# How long before a node logs slow queries. Select queries that take longer than
+# this timeout to execute, will generate an aggregated log message, so that slow queries
+# can be identified. Set this value to zero to disable slow query logging.
+slow_query_log_timeout_in_ms: 500
+
+# Enable operation timeout information exchange between nodes to accurately
+# measure request timeouts. If disabled, replicas will assume that requests
+# were forwarded to them instantly by the coordinator, which means that
+# under overload conditions we will waste that much extra time processing
+# already-timed-out requests.
+#
+# Warning: before enabling this property make sure to ntp is installed
+# and the times are synchronized between the nodes.
+cross_node_timeout: false
+
+# Set keep-alive period for streaming
+# This node will send a keep-alive message periodically with this period.
+# If the node does not receive a keep-alive message from the peer for
+# 2 keep-alive cycles the stream session times out and fail
+# Default value is 300s (5 minutes), which means stalled stream
+# times out in 10 minutes by default
+# streaming_keep_alive_period_in_secs: 300
+
+# phi value that must be reached for a host to be marked down.
+# most users should never need to adjust this.
+# phi_convict_threshold: 8
+
+# endpoint_snitch -- Set this to a class that implements
+# IEndpointSnitch. The snitch has two functions:
+#
+# - it teaches Cassandra enough about your network topology to route
+# requests efficiently
+# - it allows Cassandra to spread replicas around your cluster to avoid
+# correlated failures. It does this by grouping machines into
+# "datacenters" and "racks." Cassandra will do its best not to have
+# more than one replica on the same "rack" (which may not actually
+# be a physical location)
+#
+# CASSANDRA WILL NOT ALLOW YOU TO SWITCH TO AN INCOMPATIBLE SNITCH
+# ONCE DATA IS INSERTED INTO THE CLUSTER. This would cause data loss.
+# This means that if you start with the default SimpleSnitch, which
+# locates every node on "rack1" in "datacenter1", your only options
+# if you need to add another datacenter are GossipingPropertyFileSnitch
+# (and the older PFS). From there, if you want to migrate to an
+# incompatible snitch like Ec2Snitch you can do it by adding new nodes
+# under Ec2Snitch (which will locate them in a new "datacenter") and
+# decommissioning the old ones.
+#
+# Out of the box, Cassandra provides:
+#
+# SimpleSnitch:
+# Treats Strategy order as proximity. This can improve cache
+# locality when disabling read repair. Only appropriate for
+# single-datacenter deployments.
+#
+# GossipingPropertyFileSnitch
+# This should be your go-to snitch for production use. The rack
+# and datacenter for the local node are defined in
+# cassandra-rackdc.properties and propagated to other nodes via
+# gossip. If cassandra-topology.properties exists, it is used as a
+# fallback, allowing migration from the PropertyFileSnitch.
+#
+# PropertyFileSnitch:
+# Proximity is determined by rack and data center, which are
+# explicitly configured in cassandra-topology.properties.
+#
+# Ec2Snitch:
+# Appropriate for EC2 deployments in a single Region. Loads Region
+# and Availability Zone information from the EC2 API. The Region is
+# treated as the datacenter, and the Availability Zone as the rack.
+# Only private IPs are used, so this will not work across multiple
+# Regions.
+#
+# Ec2MultiRegionSnitch:
+# Uses public IPs as broadcast_address to allow cross-region
+# connectivity. (Thus, you should set seed addresses to the public
+# IP as well.) You will need to open the storage_port or
+# ssl_storage_port on the public IP firewall. (For intra-Region
+# traffic, Cassandra will switch to the private IP after
+# establishing a connection.)
+#
+# RackInferringSnitch:
+# Proximity is determined by rack and data center, which are
+# assumed to correspond to the 3rd and 2nd octet of each node's IP
+# address, respectively. Unless this happens to match your
+# deployment conventions, this is best used as an example of
+# writing a custom Snitch class and is provided in that spirit.
+#
+# You can use a custom Snitch by setting this to the full class name
+# of the snitch, which will be assumed to be on your classpath.
+endpoint_snitch: SimpleSnitch
+
+# controls how often to perform the more expensive part of host score
+# calculation
+dynamic_snitch_update_interval_in_ms: 100
+# controls how often to reset all host scores, allowing a bad host to
+# possibly recover
+dynamic_snitch_reset_interval_in_ms: 600000
+# if set greater than zero and read_repair_chance is < 1.0, this will allow
+# 'pinning' of replicas to hosts in order to increase cache capacity.
+# The badness threshold will control how much worse the pinned host has to be
+# before the dynamic snitch will prefer other replicas over it. This is
+# expressed as a double which represents a percentage. Thus, a value of
+# 0.2 means Cassandra would continue to prefer the static snitch values
+# until the pinned host was 20% worse than the fastest.
+dynamic_snitch_badness_threshold: 0.1
+
+# request_scheduler -- Set this to a class that implements
+# RequestScheduler, which will schedule incoming client requests
+# according to the specific policy. This is useful for multi-tenancy
+# with a single Cassandra cluster.
+# NOTE: This is specifically for requests from the client and does
+# not affect inter node communication.
+# org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place
+# org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of
+# client requests to a node with a separate queue for each
+# request_scheduler_id. The scheduler is further customized by
+# request_scheduler_options as described below.
+request_scheduler: org.apache.cassandra.scheduler.NoScheduler
+
+# Scheduler Options vary based on the type of scheduler
+#
+# NoScheduler
+# Has no options
+#
+# RoundRobin
+# throttle_limit
+# The throttle_limit is the number of in-flight
+# requests per client. Requests beyond
+# that limit are queued up until
+# running requests can complete.
+# The value of 80 here is twice the number of
+# concurrent_reads + concurrent_writes.
+# default_weight
+# default_weight is optional and allows for
+# overriding the default which is 1.
+# weights
+# Weights are optional and will default to 1 or the
+# overridden default_weight. The weight translates into how
+# many requests are handled during each turn of the
+# RoundRobin, based on the scheduler id.
+#
+# request_scheduler_options:
+# throttle_limit: 80
+# default_weight: 5
+# weights:
+# Keyspace1: 1
+# Keyspace2: 5
+
+# request_scheduler_id -- An identifier based on which to perform
+# the request scheduling. Currently the only valid option is keyspace.
+# request_scheduler_id: keyspace
+
+# Enable or disable inter-node encryption
+# JVM defaults for supported SSL socket protocols and cipher suites can
+# be replaced using custom encryption options. This is not recommended
+# unless you have policies in place that dictate certain settings, or
+# need to disable vulnerable ciphers or protocols in case the JVM cannot
+# be updated.
+# FIPS compliant settings can be configured at JVM level and should not
+# involve changing encryption settings here:
+# https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/FIPS.html
+# *NOTE* No custom encryption options are enabled at the moment
+# The available internode options are : all, none, dc, rack
+#
+# If set to dc cassandra will encrypt the traffic between the DCs
+# If set to rack cassandra will encrypt the traffic between the racks
+#
+# The passwords used in these options must match the passwords used when generating
+# the keystore and truststore. For instructions on generating these files, see:
+# http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore
+#
+server_encryption_options:
+ internode_encryption: none
+ keystore: conf/.keystore
+ keystore_password: cassandra
+ truststore: conf/.truststore
+ truststore_password: cassandra
+ # More advanced defaults below:
+ # protocol: TLS
+ # algorithm: SunX509
+ # store_type: JKS
+ # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA]
+ # require_client_auth: false
+ # require_endpoint_verification: false
+
+# enable or disable client/server encryption.
+client_encryption_options:
+ enabled: false
+ # If enabled and optional is set to true encrypted and unencrypted connections are handled.
+ optional: false
+ keystore: conf/.keystore
+ keystore_password: cassandra
+ # require_client_auth: false
+ # Set trustore and truststore_password if require_client_auth is true
+ # truststore: conf/.truststore
+ # truststore_password: cassandra
+ # More advanced defaults below:
+ # protocol: TLS
+ # algorithm: SunX509
+ # store_type: JKS
+ # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA]
+
+# internode_compression controls whether traffic between nodes is
+# compressed.
+# Can be:
+#
+# all
+# all traffic is compressed
+#
+# dc
+# traffic between different datacenters is compressed
+#
+# none
+# nothing is compressed.
+internode_compression: dc
+
+# Enable or disable tcp_nodelay for inter-dc communication.
+# Disabling it will result in larger (but fewer) network packets being sent,
+# reducing overhead from the TCP protocol itself, at the cost of increasing
+# latency if you block for cross-datacenter responses.
+inter_dc_tcp_nodelay: false
+
+# TTL for different trace types used during logging of the repair process.
+tracetype_query_ttl: 86400
+tracetype_repair_ttl: 604800
+
+# By default, Cassandra logs GC Pauses greater than 200 ms at INFO level
+# This threshold can be adjusted to minimize logging if necessary
+# gc_log_threshold_in_ms: 200
+
+# If unset, all GC Pauses greater than gc_log_threshold_in_ms will log at
+# INFO level
+# UDFs (user defined functions) are disabled by default.
+# As of Cassandra 3.0 there is a sandbox in place that should prevent execution of evil code.
+enable_user_defined_functions: false
+
+# Enables scripted UDFs (JavaScript UDFs).
+# Java UDFs are always enabled, if enable_user_defined_functions is true.
+# Enable this option to be able to use UDFs with "language javascript" or any custom JSR-223 provider.
+# This option has no effect, if enable_user_defined_functions is false.
+enable_scripted_user_defined_functions: false
+
+# The default Windows kernel timer and scheduling resolution is 15.6ms for power conservation.
+# Lowering this value on Windows can provide much tighter latency and better throughput, however
+# some virtualized environments may see a negative performance impact from changing this setting
+# below their system default. The sysinternals 'clockres' tool can confirm your system's default
+# setting.
+windows_timer_interval: 1
+
+
+# Enables encrypting data at-rest (on disk). Different key providers can be plugged in, but the default reads from
+# a JCE-style keystore. A single keystore can hold multiple keys, but the one referenced by
+# the "key_alias" is the only key that will be used for encrypt operations; previously used keys
+# can still (and should!) be in the keystore and will be used on decrypt operations
+# (to handle the case of key rotation).
+#
+# It is strongly recommended to download and install Java Cryptography Extension (JCE)
+# Unlimited Strength Jurisdiction Policy Files for your version of the JDK.
+# (current link: http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html)
+#
+# Currently, only the following file types are supported for transparent data encryption, although
+# more are coming in future cassandra releases: commitlog, hints
+transparent_data_encryption_options:
+ enabled: false
+ chunk_length_kb: 64
+ cipher: AES/CBC/PKCS5Padding
+ key_alias: testing:1
+ # CBC IV length for AES needs to be 16 bytes (which is also the default size)
+ # iv_length: 16
+ key_provider:
+ - class_name: org.apache.cassandra.security.JKSKeyProvider
+ parameters:
+ - keystore: conf/.keystore
+ keystore_password: cassandra
+ store_type: JCEKS
+ key_password: cassandra
+
+
+#####################
+# SAFETY THRESHOLDS #
+#####################
+
+# When executing a scan, within or across a partition, we need to keep the
+# tombstones seen in memory so we can return them to the coordinator, which
+# will use them to make sure other replicas also know about the deleted rows.
+# With workloads that generate a lot of tombstones, this can cause performance
+# problems and even exhaust the server heap.
+# (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets)
+# Adjust the thresholds here if you understand the dangers and want to
+# scan more tombstones anyway. These thresholds may also be adjusted at runtime
+# using the StorageService mbean.
+tombstone_warn_threshold: 1000
+tombstone_failure_threshold: 100000
+
+# Log WARN on any multiple-partition batch size exceeding this value. 5kb per batch by default.
+# Caution should be taken on increasing the size of this threshold as it can lead to node instability.
+batch_size_warn_threshold_in_kb: 5
+
+# Fail any multiple-partition batch exceeding this value. 50kb (10x warn threshold) by default.
+batch_size_fail_threshold_in_kb: 50
+
+# Log WARN on any batches not of type LOGGED than span across more partitions than this limit
+unlogged_batch_across_partitions_warn_threshold: 10
+
+# Log a warning when compacting partitions larger than this value
+compaction_large_partition_warning_threshold_mb: 100
+
+# GC Pauses greater than gc_warn_threshold_in_ms will be logged at WARN level
+# Adjust the threshold based on your application throughput requirement
+# By default, Cassandra logs GC Pauses greater than 200 ms at INFO level
+gc_warn_threshold_in_ms: 1000
+
+# Maximum size of any value in SSTables. Safety measure to detect SSTable corruption
+# early. Any value size larger than this threshold will result into marking an SSTable
+# as corrupted. This should be positive and less than 2048.
+# max_value_size_in_mb: 256
+
+# Back-pressure settings #
+# If enabled, the coordinator will apply the back-pressure strategy specified below to each mutation
+# sent to replicas, with the aim of reducing pressure on overloaded replicas.
+back_pressure_enabled: false
+# The back-pressure strategy applied.
+# The default implementation, RateBasedBackPressure, takes three arguments:
+# high ratio, factor, and flow type, and uses the ratio between incoming mutation responses and outgoing mutation requests.
+# If below high ratio, outgoing mutations are rate limited according to the incoming rate decreased by the given factor;
+# if above high ratio, the rate limiting is increased by the given factor;
+# such factor is usually best configured between 1 and 10, use larger values for a faster recovery
+# at the expense of potentially more dropped mutations;
+# the rate limiting is applied according to the flow type: if FAST, it's rate limited at the speed of the fastest replica,
+# if SLOW at the speed of the slowest one.
+# New strategies can be added. Implementors need to implement org.apache.cassandra.net.BackpressureStrategy and
+# provide a public constructor accepting a Map.
+back_pressure_strategy:
+ - class_name: org.apache.cassandra.net.RateBasedBackPressure
+ parameters:
+ - high_ratio: 0.90
+ factor: 5
+ flow: FAST
+
+# Coalescing Strategies #
+# Coalescing multiples messages turns out to significantly boost message processing throughput (think doubling or more).
+# On bare metal, the floor for packet processing throughput is high enough that many applications won't notice, but in
+# virtualized environments, the point at which an application can be bound by network packet processing can be
+# surprisingly low compared to the throughput of task processing that is possible inside a VM. It's not that bare metal
+# doesn't benefit from coalescing messages, it's that the number of packets a bare metal network interface can process
+# is sufficient for many applications such that no load starvation is experienced even without coalescing.
+# There are other benefits to coalescing network messages that are harder to isolate with a simple metric like messages
+# per second. By coalescing multiple tasks together, a network thread can process multiple messages for the cost of one
+# trip to read from a socket, and all the task submission work can be done at the same time reducing context switching
+# and increasing cache friendliness of network message processing.
+# See CASSANDRA-8692 for details.
+
+# Strategy to use for coalescing messages in OutboundTcpConnection.
+# Can be fixed, movingaverage, timehorizon, disabled (default).
+# You can also specify a subclass of CoalescingStrategies.CoalescingStrategy by name.
+# otc_coalescing_strategy: DISABLED
+
+# How many microseconds to wait for coalescing. For fixed strategy this is the amount of time after the first
+# message is received before it will be sent with any accompanying messages. For moving average this is the
+# maximum amount of time that will be waited as well as the interval at which messages must arrive on average
+# for coalescing to be enabled.
+# otc_coalescing_window_us: 200
+
+# Do not try to coalesce messages if we already got that many messages. This should be more than 2 and less than 128.
+# otc_coalescing_enough_coalesced_messages: 8
+
+# How many milliseconds to wait between two expiration runs on the backlog (queue) of the OutboundTcpConnection.
+# Expiration is done if messages are piling up in the backlog. Droppable messages are expired to free the memory
+# taken by expired messages. The interval should be between 0 and 1000, and in most installations the default value
+# will be appropriate. A smaller value could potentially expire messages slightly sooner at the expense of more CPU
+# time and queue contention while iterating the backlog of messages.
+# An interval of 0 disables any wait time, which is the behavior of former Cassandra versions.
+#
+# otc_backlog_expiration_interval_ms: 200
diff --git a/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.cer b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.cer
new file mode 100644
index 00000000000..7a6aee6dec4
Binary files /dev/null and b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.cer differ
diff --git a/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.yaml b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.yaml
new file mode 100644
index 00000000000..116ba3429cf
--- /dev/null
+++ b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.yaml
@@ -0,0 +1,1233 @@
+# Cassandra storage config YAML
+
+# NOTE:
+# See http://wiki.apache.org/cassandra/StorageConfiguration for
+# full explanations of configuration directives
+# /NOTE
+
+# The name of the cluster. This is mainly used to prevent machines in
+# one logical cluster from joining another.
+cluster_name: 'Test Cluster Integration Test'
+
+# This defines the number of tokens randomly assigned to this node on the ring
+# The more tokens, relative to other nodes, the larger the proportion of data
+# that this node will store. You probably want all nodes to have the same number
+# of tokens assuming they have equal hardware capability.
+#
+# If you leave this unspecified, Cassandra will use the default of 1 token for legacy compatibility,
+# and will use the initial_token as described below.
+#
+# Specifying initial_token will override this setting on the node's initial start,
+# on subsequent starts, this setting will apply even if initial token is set.
+#
+# If you already have a cluster with 1 token per node, and wish to migrate to
+# multiple tokens per node, see http://wiki.apache.org/cassandra/Operations
+num_tokens: 256
+
+# Triggers automatic allocation of num_tokens tokens for this node. The allocation
+# algorithm attempts to choose tokens in a way that optimizes replicated load over
+# the nodes in the datacenter for the replication strategy used by the specified
+# keyspace.
+#
+# The load assigned to each node will be close to proportional to its number of
+# vnodes.
+#
+# Only supported with the Murmur3Partitioner.
+# allocate_tokens_for_keyspace: KEYSPACE
+
+# initial_token allows you to specify tokens manually. While you can use it with
+# vnodes (num_tokens > 1, above) -- in which case you should provide a
+# comma-separated list -- it's primarily used when adding nodes to legacy clusters
+# that do not have vnodes enabled.
+# initial_token:
+
+# See http://wiki.apache.org/cassandra/HintedHandoff
+# May either be "true" or "false" to enable globally
+hinted_handoff_enabled: true
+
+# When hinted_handoff_enabled is true, a black list of data centers that will not
+# perform hinted handoff
+# hinted_handoff_disabled_datacenters:
+# - DC1
+# - DC2
+
+# this defines the maximum amount of time a dead host will have hints
+# generated. After it has been dead this long, new hints for it will not be
+# created until it has been seen alive and gone down again.
+max_hint_window_in_ms: 10800000 # 3 hours
+
+# Maximum throttle in KBs per second, per delivery thread. This will be
+# reduced proportionally to the number of nodes in the cluster. (If there
+# are two nodes in the cluster, each delivery thread will use the maximum
+# rate; if there are three, each will throttle to half of the maximum,
+# since we expect two nodes to be delivering hints simultaneously.)
+hinted_handoff_throttle_in_kb: 1024
+
+# Number of threads with which to deliver hints;
+# Consider increasing this number when you have multi-dc deployments, since
+# cross-dc handoff tends to be slower
+max_hints_delivery_threads: 2
+
+# Directory where Cassandra should store hints.
+# If not set, the default directory is $CASSANDRA_HOME/data/hints.
+# hints_directory: /var/lib/cassandra/hints
+
+# How often hints should be flushed from the internal buffers to disk.
+# Will *not* trigger fsync.
+hints_flush_period_in_ms: 10000
+
+# Maximum size for a single hints file, in megabytes.
+max_hints_file_size_in_mb: 128
+
+# Compression to apply to the hint files. If omitted, hints files
+# will be written uncompressed. LZ4, Snappy, and Deflate compressors
+# are supported.
+#hints_compression:
+# - class_name: LZ4Compressor
+# parameters:
+# -
+
+# Maximum throttle in KBs per second, total. This will be
+# reduced proportionally to the number of nodes in the cluster.
+batchlog_replay_throttle_in_kb: 1024
+
+# Authentication backend, implementing IAuthenticator; used to identify users
+# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator,
+# PasswordAuthenticator}.
+#
+# - AllowAllAuthenticator performs no checks - set it to disable authentication.
+# - PasswordAuthenticator relies on username/password pairs to authenticate
+# users. It keeps usernames and hashed passwords in system_auth.roles table.
+# Please increase system_auth keyspace replication factor if you use this authenticator.
+# If using PasswordAuthenticator, CassandraRoleManager must also be used (see below)
+authenticator: AllowAllAuthenticator
+
+# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions
+# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer,
+# CassandraAuthorizer}.
+#
+# - AllowAllAuthorizer allows any action to any user - set it to disable authorization.
+# - CassandraAuthorizer stores permissions in system_auth.role_permissions table. Please
+# increase system_auth keyspace replication factor if you use this authorizer.
+authorizer: AllowAllAuthorizer
+
+# Part of the Authentication & Authorization backend, implementing IRoleManager; used
+# to maintain grants and memberships between roles.
+# Out of the box, Cassandra provides org.apache.cassandra.auth.CassandraRoleManager,
+# which stores role information in the system_auth keyspace. Most functions of the
+# IRoleManager require an authenticated login, so unless the configured IAuthenticator
+# actually implements authentication, most of this functionality will be unavailable.
+#
+# - CassandraRoleManager stores role data in the system_auth keyspace. Please
+# increase system_auth keyspace replication factor if you use this role manager.
+role_manager: CassandraRoleManager
+
+# Validity period for roles cache (fetching granted roles can be an expensive
+# operation depending on the role manager, CassandraRoleManager is one example)
+# Granted roles are cached for authenticated sessions in AuthenticatedUser and
+# after the period specified here, become eligible for (async) reload.
+# Defaults to 2000, set to 0 to disable caching entirely.
+# Will be disabled automatically for AllowAllAuthenticator.
+roles_validity_in_ms: 2000
+
+# Refresh interval for roles cache (if enabled).
+# After this interval, cache entries become eligible for refresh. Upon next
+# access, an async reload is scheduled and the old value returned until it
+# completes. If roles_validity_in_ms is non-zero, then this must be
+# also.
+# Defaults to the same value as roles_validity_in_ms.
+# roles_update_interval_in_ms: 2000
+
+# Validity period for permissions cache (fetching permissions can be an
+# expensive operation depending on the authorizer, CassandraAuthorizer is
+# one example). Defaults to 2000, set to 0 to disable.
+# Will be disabled automatically for AllowAllAuthorizer.
+permissions_validity_in_ms: 2000
+
+# Refresh interval for permissions cache (if enabled).
+# After this interval, cache entries become eligible for refresh. Upon next
+# access, an async reload is scheduled and the old value returned until it
+# completes. If permissions_validity_in_ms is non-zero, then this must be
+# also.
+# Defaults to the same value as permissions_validity_in_ms.
+# permissions_update_interval_in_ms: 2000
+
+# Validity period for credentials cache. This cache is tightly coupled to
+# the provided PasswordAuthenticator implementation of IAuthenticator. If
+# another IAuthenticator implementation is configured, this cache will not
+# be automatically used and so the following settings will have no effect.
+# Please note, credentials are cached in their encrypted form, so while
+# activating this cache may reduce the number of queries made to the
+# underlying table, it may not bring a significant reduction in the
+# latency of individual authentication attempts.
+# Defaults to 2000, set to 0 to disable credentials caching.
+credentials_validity_in_ms: 2000
+
+# Refresh interval for credentials cache (if enabled).
+# After this interval, cache entries become eligible for refresh. Upon next
+# access, an async reload is scheduled and the old value returned until it
+# completes. If credentials_validity_in_ms is non-zero, then this must be
+# also.
+# Defaults to the same value as credentials_validity_in_ms.
+# credentials_update_interval_in_ms: 2000
+
+# The partitioner is responsible for distributing groups of rows (by
+# partition key) across nodes in the cluster. You should leave this
+# alone for new clusters. The partitioner can NOT be changed without
+# reloading all data, so when upgrading you should set this to the
+# same partitioner you were already using.
+#
+# Besides Murmur3Partitioner, partitioners included for backwards
+# compatibility include RandomPartitioner, ByteOrderedPartitioner, and
+# OrderPreservingPartitioner.
+#
+partitioner: org.apache.cassandra.dht.Murmur3Partitioner
+
+# Directories where Cassandra should store data on disk. Cassandra
+# will spread data evenly across them, subject to the granularity of
+# the configured compaction strategy.
+# If not set, the default directory is $CASSANDRA_HOME/data/data.
+data_file_directories:
+ - /var/lib/cassandra/data
+
+# commit log. when running on magnetic HDD, this should be a
+# separate spindle than the data directories.
+# If not set, the default directory is $CASSANDRA_HOME/data/commitlog.
+commitlog_directory: /var/lib/cassandra/commitlog
+
+# Enable / disable CDC functionality on a per-node basis. This modifies the logic used
+# for write path allocation rejection (standard: never reject. cdc: reject Mutation
+# containing a CDC-enabled table if at space limit in cdc_raw_directory).
+cdc_enabled: false
+
+# CommitLogSegments are moved to this directory on flush if cdc_enabled: true and the
+# segment contains mutations for a CDC-enabled table. This should be placed on a
+# separate spindle than the data directories. If not set, the default directory is
+# $CASSANDRA_HOME/data/cdc_raw.
+# cdc_raw_directory: /var/lib/cassandra/cdc_raw
+
+# Policy for data disk failures:
+#
+# die
+# shut down gossip and client transports and kill the JVM for any fs errors or
+# single-sstable errors, so the node can be replaced.
+#
+# stop_paranoid
+# shut down gossip and client transports even for single-sstable errors,
+# kill the JVM for errors during startup.
+#
+# stop
+# shut down gossip and client transports, leaving the node effectively dead, but
+# can still be inspected via JMX, kill the JVM for errors during startup.
+#
+# best_effort
+# stop using the failed disk and respond to requests based on
+# remaining available sstables. This means you WILL see obsolete
+# data at CL.ONE!
+#
+# ignore
+# ignore fatal errors and let requests fail, as in pre-1.2 Cassandra
+disk_failure_policy: stop
+
+# Policy for commit disk failures:
+#
+# die
+# shut down gossip and Thrift and kill the JVM, so the node can be replaced.
+#
+# stop
+# shut down gossip and Thrift, leaving the node effectively dead, but
+# can still be inspected via JMX.
+#
+# stop_commit
+# shutdown the commit log, letting writes collect but
+# continuing to service reads, as in pre-2.0.5 Cassandra
+#
+# ignore
+# ignore fatal errors and let the batches fail
+commit_failure_policy: stop
+
+# Maximum size of the native protocol prepared statement cache
+#
+# Valid values are either "auto" (omitting the value) or a value greater 0.
+#
+# Note that specifying a too large value will result in long running GCs and possbily
+# out-of-memory errors. Keep the value at a small fraction of the heap.
+#
+# If you constantly see "prepared statements discarded in the last minute because
+# cache limit reached" messages, the first step is to investigate the root cause
+# of these messages and check whether prepared statements are used correctly -
+# i.e. use bind markers for variable parts.
+#
+# Do only change the default value, if you really have more prepared statements than
+# fit in the cache. In most cases it is not neccessary to change this value.
+# Constantly re-preparing statements is a performance penalty.
+#
+# Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater
+prepared_statements_cache_size_mb:
+
+# Maximum size of the Thrift prepared statement cache
+#
+# If you do not use Thrift at all, it is safe to leave this value at "auto".
+#
+# See description of 'prepared_statements_cache_size_mb' above for more information.
+#
+# Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater
+thrift_prepared_statements_cache_size_mb:
+
+# Maximum size of the key cache in memory.
+#
+# Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the
+# minimum, sometimes more. The key cache is fairly tiny for the amount of
+# time it saves, so it's worthwhile to use it at large numbers.
+# The row cache saves even more time, but must contain the entire row,
+# so it is extremely space-intensive. It's best to only use the
+# row cache if you have hot rows or static rows.
+#
+# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup.
+#
+# Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache.
+key_cache_size_in_mb:
+
+# Duration in seconds after which Cassandra should
+# save the key cache. Caches are saved to saved_caches_directory as
+# specified in this configuration file.
+#
+# Saved caches greatly improve cold-start speeds, and is relatively cheap in
+# terms of I/O for the key cache. Row cache saving is much more expensive and
+# has limited use.
+#
+# Default is 14400 or 4 hours.
+key_cache_save_period: 14400
+
+# Number of keys from the key cache to save
+# Disabled by default, meaning all keys are going to be saved
+# key_cache_keys_to_save: 100
+
+# Row cache implementation class name. Available implementations:
+#
+# org.apache.cassandra.cache.OHCProvider
+# Fully off-heap row cache implementation (default).
+#
+# org.apache.cassandra.cache.SerializingCacheProvider
+# This is the row cache implementation availabile
+# in previous releases of Cassandra.
+# row_cache_class_name: org.apache.cassandra.cache.OHCProvider
+
+# Maximum size of the row cache in memory.
+# Please note that OHC cache implementation requires some additional off-heap memory to manage
+# the map structures and some in-flight memory during operations before/after cache entries can be
+# accounted against the cache capacity. This overhead is usually small compared to the whole capacity.
+# Do not specify more memory that the system can afford in the worst usual situation and leave some
+# headroom for OS block level cache. Do never allow your system to swap.
+#
+# Default value is 0, to disable row caching.
+row_cache_size_in_mb: 0
+
+# Duration in seconds after which Cassandra should save the row cache.
+# Caches are saved to saved_caches_directory as specified in this configuration file.
+#
+# Saved caches greatly improve cold-start speeds, and is relatively cheap in
+# terms of I/O for the key cache. Row cache saving is much more expensive and
+# has limited use.
+#
+# Default is 0 to disable saving the row cache.
+row_cache_save_period: 0
+
+# Number of keys from the row cache to save.
+# Specify 0 (which is the default), meaning all keys are going to be saved
+# row_cache_keys_to_save: 100
+
+# Maximum size of the counter cache in memory.
+#
+# Counter cache helps to reduce counter locks' contention for hot counter cells.
+# In case of RF = 1 a counter cache hit will cause Cassandra to skip the read before
+# write entirely. With RF > 1 a counter cache hit will still help to reduce the duration
+# of the lock hold, helping with hot counter cell updates, but will not allow skipping
+# the read entirely. Only the local (clock, count) tuple of a counter cell is kept
+# in memory, not the whole counter, so it's relatively cheap.
+#
+# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup.
+#
+# Default value is empty to make it "auto" (min(2.5% of Heap (in MB), 50MB)). Set to 0 to disable counter cache.
+# NOTE: if you perform counter deletes and rely on low gcgs, you should disable the counter cache.
+counter_cache_size_in_mb:
+
+# Duration in seconds after which Cassandra should
+# save the counter cache (keys only). Caches are saved to saved_caches_directory as
+# specified in this configuration file.
+#
+# Default is 7200 or 2 hours.
+counter_cache_save_period: 7200
+
+# Number of keys from the counter cache to save
+# Disabled by default, meaning all keys are going to be saved
+# counter_cache_keys_to_save: 100
+
+# saved caches
+# If not set, the default directory is $CASSANDRA_HOME/data/saved_caches.
+saved_caches_directory: /var/lib/cassandra/saved_caches
+
+# commitlog_sync may be either "periodic" or "batch."
+#
+# When in batch mode, Cassandra won't ack writes until the commit log
+# has been fsynced to disk. It will wait
+# commitlog_sync_batch_window_in_ms milliseconds between fsyncs.
+# This window should be kept short because the writer threads will
+# be unable to do extra work while waiting. (You may need to increase
+# concurrent_writes for the same reason.)
+#
+# commitlog_sync: batch
+# commitlog_sync_batch_window_in_ms: 2
+#
+# the other option is "periodic" where writes may be acked immediately
+# and the CommitLog is simply synced every commitlog_sync_period_in_ms
+# milliseconds.
+commitlog_sync: periodic
+commitlog_sync_period_in_ms: 10000
+
+# The size of the individual commitlog file segments. A commitlog
+# segment may be archived, deleted, or recycled once all the data
+# in it (potentially from each columnfamily in the system) has been
+# flushed to sstables.
+#
+# The default size is 32, which is almost always fine, but if you are
+# archiving commitlog segments (see commitlog_archiving.properties),
+# then you probably want a finer granularity of archiving; 8 or 16 MB
+# is reasonable.
+# Max mutation size is also configurable via max_mutation_size_in_kb setting in
+# cassandra.yaml. The default is half the size commitlog_segment_size_in_mb * 1024.
+# This should be positive and less than 2048.
+#
+# NOTE: If max_mutation_size_in_kb is set explicitly then commitlog_segment_size_in_mb must
+# be set to at least twice the size of max_mutation_size_in_kb / 1024
+#
+commitlog_segment_size_in_mb: 32
+
+# Compression to apply to the commit log. If omitted, the commit log
+# will be written uncompressed. LZ4, Snappy, and Deflate compressors
+# are supported.
+# commitlog_compression:
+# - class_name: LZ4Compressor
+# parameters:
+# -
+
+# any class that implements the SeedProvider interface and has a
+# constructor that takes a Map of parameters will do.
+seed_provider:
+ # Addresses of hosts that are deemed contact points.
+ # Cassandra nodes use this list of hosts to find each other and learn
+ # the topology of the ring. You must change this if you are running
+ # multiple nodes!
+ - class_name: org.apache.cassandra.locator.SimpleSeedProvider
+ parameters:
+ # seeds is actually a comma-delimited list of addresses.
+ # Ex: ",,"
+ - seeds: "172.17.0.2"
+
+# For workloads with more data than can fit in memory, Cassandra's
+# bottleneck will be reads that need to fetch data from
+# disk. "concurrent_reads" should be set to (16 * number_of_drives) in
+# order to allow the operations to enqueue low enough in the stack
+# that the OS and drives can reorder them. Same applies to
+# "concurrent_counter_writes", since counter writes read the current
+# values before incrementing and writing them back.
+#
+# On the other hand, since writes are almost never IO bound, the ideal
+# number of "concurrent_writes" is dependent on the number of cores in
+# your system; (8 * number_of_cores) is a good rule of thumb.
+concurrent_reads: 32
+concurrent_writes: 32
+concurrent_counter_writes: 32
+
+# For materialized view writes, as there is a read involved, so this should
+# be limited by the less of concurrent reads or concurrent writes.
+concurrent_materialized_view_writes: 32
+
+# Maximum memory to use for sstable chunk cache and buffer pooling.
+# 32MB of this are reserved for pooling buffers, the rest is used as an
+# cache that holds uncompressed sstable chunks.
+# Defaults to the smaller of 1/4 of heap or 512MB. This pool is allocated off-heap,
+# so is in addition to the memory allocated for heap. The cache also has on-heap
+# overhead which is roughly 128 bytes per chunk (i.e. 0.2% of the reserved size
+# if the default 64k chunk size is used).
+# Memory is only allocated when needed.
+# file_cache_size_in_mb: 512
+
+# Flag indicating whether to allocate on or off heap when the sstable buffer
+# pool is exhausted, that is when it has exceeded the maximum memory
+# file_cache_size_in_mb, beyond which it will not cache buffers but allocate on request.
+
+# buffer_pool_use_heap_if_exhausted: true
+
+# The strategy for optimizing disk read
+# Possible values are:
+# ssd (for solid state disks, the default)
+# spinning (for spinning disks)
+# disk_optimization_strategy: ssd
+
+# Total permitted memory to use for memtables. Cassandra will stop
+# accepting writes when the limit is exceeded until a flush completes,
+# and will trigger a flush based on memtable_cleanup_threshold
+# If omitted, Cassandra will set both to 1/4 the size of the heap.
+# memtable_heap_space_in_mb: 2048
+# memtable_offheap_space_in_mb: 2048
+
+# memtable_cleanup_threshold is deprecated. The default calculation
+# is the only reasonable choice. See the comments on memtable_flush_writers
+# for more information.
+#
+# Ratio of occupied non-flushing memtable size to total permitted size
+# that will trigger a flush of the largest memtable. Larger mct will
+# mean larger flushes and hence less compaction, but also less concurrent
+# flush activity which can make it difficult to keep your disks fed
+# under heavy write load.
+#
+# memtable_cleanup_threshold defaults to 1 / (memtable_flush_writers + 1)
+# memtable_cleanup_threshold: 0.11
+
+# Specify the way Cassandra allocates and manages memtable memory.
+# Options are:
+#
+# heap_buffers
+# on heap nio buffers
+#
+# offheap_buffers
+# off heap (direct) nio buffers
+#
+# offheap_objects
+# off heap objects
+memtable_allocation_type: heap_buffers
+
+# Total space to use for commit logs on disk.
+#
+# If space gets above this value, Cassandra will flush every dirty CF
+# in the oldest segment and remove it. So a small total commitlog space
+# will tend to cause more flush activity on less-active columnfamilies.
+#
+# The default value is the smaller of 8192, and 1/4 of the total space
+# of the commitlog volume.
+#
+# commitlog_total_space_in_mb: 8192
+
+# This sets the number of memtable flush writer threads per disk
+# as well as the total number of memtables that can be flushed concurrently.
+# These are generally a combination of compute and IO bound.
+#
+# Memtable flushing is more CPU efficient than memtable ingest and a single thread
+# can keep up with the ingest rate of a whole server on a single fast disk
+# until it temporarily becomes IO bound under contention typically with compaction.
+# At that point you need multiple flush threads. At some point in the future
+# it may become CPU bound all the time.
+#
+# You can tell if flushing is falling behind using the MemtablePool.BlockedOnAllocation
+# metric which should be 0, but will be non-zero if threads are blocked waiting on flushing
+# to free memory.
+#
+# memtable_flush_writers defaults to two for a single data directory.
+# This means that two memtables can be flushed concurrently to the single data directory.
+# If you have multiple data directories the default is one memtable flushing at a time
+# but the flush will use a thread per data directory so you will get two or more writers.
+#
+# Two is generally enough to flush on a fast disk [array] mounted as a single data directory.
+# Adding more flush writers will result in smaller more frequent flushes that introduce more
+# compaction overhead.
+#
+# There is a direct tradeoff between number of memtables that can be flushed concurrently
+# and flush size and frequency. More is not better you just need enough flush writers
+# to never stall waiting for flushing to free memory.
+#
+#memtable_flush_writers: 2
+
+# Total space to use for change-data-capture logs on disk.
+#
+# If space gets above this value, Cassandra will throw WriteTimeoutException
+# on Mutations including tables with CDC enabled. A CDCCompactor is responsible
+# for parsing the raw CDC logs and deleting them when parsing is completed.
+#
+# The default value is the min of 4096 mb and 1/8th of the total space
+# of the drive where cdc_raw_directory resides.
+# cdc_total_space_in_mb: 4096
+
+# When we hit our cdc_raw limit and the CDCCompactor is either running behind
+# or experiencing backpressure, we check at the following interval to see if any
+# new space for cdc-tracked tables has been made available. Default to 250ms
+# cdc_free_space_check_interval_ms: 250
+
+# A fixed memory pool size in MB for for SSTable index summaries. If left
+# empty, this will default to 5% of the heap size. If the memory usage of
+# all index summaries exceeds this limit, SSTables with low read rates will
+# shrink their index summaries in order to meet this limit. However, this
+# is a best-effort process. In extreme conditions Cassandra may need to use
+# more than this amount of memory.
+index_summary_capacity_in_mb:
+
+# How frequently index summaries should be resampled. This is done
+# periodically to redistribute memory from the fixed-size pool to sstables
+# proportional their recent read rates. Setting to -1 will disable this
+# process, leaving existing index summaries at their current sampling level.
+index_summary_resize_interval_in_minutes: 60
+
+# Whether to, when doing sequential writing, fsync() at intervals in
+# order to force the operating system to flush the dirty
+# buffers. Enable this to avoid sudden dirty buffer flushing from
+# impacting read latencies. Almost always a good idea on SSDs; not
+# necessarily on platters.
+trickle_fsync: false
+trickle_fsync_interval_in_kb: 10240
+
+# TCP port, for commands and data
+# For security reasons, you should not expose this port to the internet. Firewall it if needed.
+storage_port: 7000
+
+# SSL port, for encrypted communication. Unused unless enabled in
+# encryption_options
+# For security reasons, you should not expose this port to the internet. Firewall it if needed.
+ssl_storage_port: 7001
+
+# Address or interface to bind to and tell other Cassandra nodes to connect to.
+# You _must_ change this if you want multiple nodes to be able to communicate!
+#
+# Set listen_address OR listen_interface, not both.
+#
+# Leaving it blank leaves it up to InetAddress.getLocalHost(). This
+# will always do the Right Thing _if_ the node is properly configured
+# (hostname, name resolution, etc), and the Right Thing is to use the
+# address associated with the hostname (it might not be).
+#
+# Setting listen_address to 0.0.0.0 is always wrong.
+#
+listen_address: 172.17.0.2
+
+# Set listen_address OR listen_interface, not both. Interfaces must correspond
+# to a single address, IP aliasing is not supported.
+# listen_interface: eth0
+
+# If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address
+# you can specify which should be chosen using listen_interface_prefer_ipv6. If false the first ipv4
+# address will be used. If true the first ipv6 address will be used. Defaults to false preferring
+# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6.
+# listen_interface_prefer_ipv6: false
+
+# Address to broadcast to other Cassandra nodes
+# Leaving this blank will set it to the same value as listen_address
+broadcast_address: 172.17.0.2
+
+# When using multiple physical network interfaces, set this
+# to true to listen on broadcast_address in addition to
+# the listen_address, allowing nodes to communicate in both
+# interfaces.
+# Ignore this property if the network configuration automatically
+# routes between the public and private networks such as EC2.
+# listen_on_broadcast_address: false
+
+# Internode authentication backend, implementing IInternodeAuthenticator;
+# used to allow/disallow connections from peer nodes.
+# internode_authenticator: org.apache.cassandra.auth.AllowAllInternodeAuthenticator
+
+# Whether to start the native transport server.
+# Please note that the address on which the native transport is bound is the
+# same as the rpc_address. The port however is different and specified below.
+start_native_transport: true
+# port for the CQL native transport to listen for clients on
+# For security reasons, you should not expose this port to the internet. Firewall it if needed.
+native_transport_port: 9042
+# Enabling native transport encryption in client_encryption_options allows you to either use
+# encryption for the standard port or to use a dedicated, additional port along with the unencrypted
+# standard native_transport_port.
+# Enabling client encryption and keeping native_transport_port_ssl disabled will use encryption
+# for native_transport_port. Setting native_transport_port_ssl to a different value
+# from native_transport_port will use encryption for native_transport_port_ssl while
+# keeping native_transport_port unencrypted.
+# native_transport_port_ssl: 9142
+# The maximum threads for handling requests when the native transport is used.
+# This is similar to rpc_max_threads though the default differs slightly (and
+# there is no native_transport_min_threads, idle threads will always be stopped
+# after 30 seconds).
+# native_transport_max_threads: 128
+#
+# The maximum size of allowed frame. Frame (requests) larger than this will
+# be rejected as invalid. The default is 256MB. If you're changing this parameter,
+# you may want to adjust max_value_size_in_mb accordingly. This should be positive and less than 2048.
+# native_transport_max_frame_size_in_mb: 256
+
+# The maximum number of concurrent client connections.
+# The default is -1, which means unlimited.
+# native_transport_max_concurrent_connections: -1
+
+# The maximum number of concurrent client connections per source ip.
+# The default is -1, which means unlimited.
+# native_transport_max_concurrent_connections_per_ip: -1
+
+# Whether to start the thrift rpc server.
+start_rpc: false
+
+# The address or interface to bind the Thrift RPC service and native transport
+# server to.
+#
+# Set rpc_address OR rpc_interface, not both.
+#
+# Leaving rpc_address blank has the same effect as on listen_address
+# (i.e. it will be based on the configured hostname of the node).
+#
+# Note that unlike listen_address, you can specify 0.0.0.0, but you must also
+# set broadcast_rpc_address to a value other than 0.0.0.0.
+#
+# For security reasons, you should not expose this port to the internet. Firewall it if needed.
+rpc_address: 0.0.0.0
+
+# Set rpc_address OR rpc_interface, not both. Interfaces must correspond
+# to a single address, IP aliasing is not supported.
+# rpc_interface: eth1
+
+# If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address
+# you can specify which should be chosen using rpc_interface_prefer_ipv6. If false the first ipv4
+# address will be used. If true the first ipv6 address will be used. Defaults to false preferring
+# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6.
+# rpc_interface_prefer_ipv6: false
+
+# port for Thrift to listen for clients on
+rpc_port: 9160
+
+# RPC address to broadcast to drivers and other Cassandra nodes. This cannot
+# be set to 0.0.0.0. If left blank, this will be set to the value of
+# rpc_address. If rpc_address is set to 0.0.0.0, broadcast_rpc_address must
+# be set.
+broadcast_rpc_address: 172.17.0.2
+
+# enable or disable keepalive on rpc/native connections
+rpc_keepalive: true
+
+# Cassandra provides two out-of-the-box options for the RPC Server:
+#
+# sync
+# One thread per thrift connection. For a very large number of clients, memory
+# will be your limiting factor. On a 64 bit JVM, 180KB is the minimum stack size
+# per thread, and that will correspond to your use of virtual memory (but physical memory
+# may be limited depending on use of stack space).
+#
+# hsha
+# Stands for "half synchronous, half asynchronous." All thrift clients are handled
+# asynchronously using a small number of threads that does not vary with the amount
+# of thrift clients (and thus scales well to many clients). The rpc requests are still
+# synchronous (one thread per active request). If hsha is selected then it is essential
+# that rpc_max_threads is changed from the default value of unlimited.
+#
+# The default is sync because on Windows hsha is about 30% slower. On Linux,
+# sync/hsha performance is about the same, with hsha of course using less memory.
+#
+# Alternatively, can provide your own RPC server by providing the fully-qualified class name
+# of an o.a.c.t.TServerFactory that can create an instance of it.
+rpc_server_type: sync
+
+# Uncomment rpc_min|max_thread to set request pool size limits.
+#
+# Regardless of your choice of RPC server (see above), the number of maximum requests in the
+# RPC thread pool dictates how many concurrent requests are possible (but if you are using the sync
+# RPC server, it also dictates the number of clients that can be connected at all).
+#
+# The default is unlimited and thus provides no protection against clients overwhelming the server. You are
+# encouraged to set a maximum that makes sense for you in production, but do keep in mind that
+# rpc_max_threads represents the maximum number of client requests this server may execute concurrently.
+#
+# rpc_min_threads: 16
+# rpc_max_threads: 2048
+
+# uncomment to set socket buffer sizes on rpc connections
+# rpc_send_buff_size_in_bytes:
+# rpc_recv_buff_size_in_bytes:
+
+# Uncomment to set socket buffer size for internode communication
+# Note that when setting this, the buffer size is limited by net.core.wmem_max
+# and when not setting it it is defined by net.ipv4.tcp_wmem
+# See also:
+# /proc/sys/net/core/wmem_max
+# /proc/sys/net/core/rmem_max
+# /proc/sys/net/ipv4/tcp_wmem
+# /proc/sys/net/ipv4/tcp_wmem
+# and 'man tcp'
+# internode_send_buff_size_in_bytes:
+
+# Uncomment to set socket buffer size for internode communication
+# Note that when setting this, the buffer size is limited by net.core.wmem_max
+# and when not setting it it is defined by net.ipv4.tcp_wmem
+# internode_recv_buff_size_in_bytes:
+
+# Frame size for thrift (maximum message length).
+thrift_framed_transport_size_in_mb: 15
+
+# Set to true to have Cassandra create a hard link to each sstable
+# flushed or streamed locally in a backups/ subdirectory of the
+# keyspace data. Removing these links is the operator's
+# responsibility.
+incremental_backups: false
+
+# Whether or not to take a snapshot before each compaction. Be
+# careful using this option, since Cassandra won't clean up the
+# snapshots for you. Mostly useful if you're paranoid when there
+# is a data format change.
+snapshot_before_compaction: false
+
+# Whether or not a snapshot is taken of the data before keyspace truncation
+# or dropping of column families. The STRONGLY advised default of true
+# should be used to provide data safety. If you set this flag to false, you will
+# lose data on truncation or drop.
+auto_snapshot: true
+
+# Granularity of the collation index of rows within a partition.
+# Increase if your rows are large, or if you have a very large
+# number of rows per partition. The competing goals are these:
+#
+# - a smaller granularity means more index entries are generated
+# and looking up rows withing the partition by collation column
+# is faster
+# - but, Cassandra will keep the collation index in memory for hot
+# rows (as part of the key cache), so a larger granularity means
+# you can cache more hot rows
+column_index_size_in_kb: 64
+
+# Per sstable indexed key cache entries (the collation index in memory
+# mentioned above) exceeding this size will not be held on heap.
+# This means that only partition information is held on heap and the
+# index entries are read from disk.
+#
+# Note that this size refers to the size of the
+# serialized index information and not the size of the partition.
+column_index_cache_size_in_kb: 2
+
+# Number of simultaneous compactions to allow, NOT including
+# validation "compactions" for anti-entropy repair. Simultaneous
+# compactions can help preserve read performance in a mixed read/write
+# workload, by mitigating the tendency of small sstables to accumulate
+# during a single long running compactions. The default is usually
+# fine and if you experience problems with compaction running too
+# slowly or too fast, you should look at
+# compaction_throughput_mb_per_sec first.
+#
+# concurrent_compactors defaults to the smaller of (number of disks,
+# number of cores), with a minimum of 2 and a maximum of 8.
+#
+# If your data directories are backed by SSD, you should increase this
+# to the number of cores.
+#concurrent_compactors: 1
+
+# Throttles compaction to the given total throughput across the entire
+# system. The faster you insert data, the faster you need to compact in
+# order to keep the sstable count down, but in general, setting this to
+# 16 to 32 times the rate you are inserting data is more than sufficient.
+# Setting this to 0 disables throttling. Note that this account for all types
+# of compaction, including validation compaction.
+compaction_throughput_mb_per_sec: 16
+
+# When compacting, the replacement sstable(s) can be opened before they
+# are completely written, and used in place of the prior sstables for
+# any range that has been written. This helps to smoothly transfer reads
+# between the sstables, reducing page cache churn and keeping hot rows hot
+sstable_preemptive_open_interval_in_mb: 50
+
+# Throttles all outbound streaming file transfers on this node to the
+# given total throughput in Mbps. This is necessary because Cassandra does
+# mostly sequential IO when streaming data during bootstrap or repair, which
+# can lead to saturating the network connection and degrading rpc performance.
+# When unset, the default is 200 Mbps or 25 MB/s.
+# stream_throughput_outbound_megabits_per_sec: 200
+
+# Throttles all streaming file transfer between the datacenters,
+# this setting allows users to throttle inter dc stream throughput in addition
+# to throttling all network stream traffic as configured with
+# stream_throughput_outbound_megabits_per_sec
+# When unset, the default is 200 Mbps or 25 MB/s
+# inter_dc_stream_throughput_outbound_megabits_per_sec: 200
+
+# How long the coordinator should wait for read operations to complete
+read_request_timeout_in_ms: 5000
+# How long the coordinator should wait for seq or index scans to complete
+range_request_timeout_in_ms: 10000
+# How long the coordinator should wait for writes to complete
+write_request_timeout_in_ms: 2000
+# How long the coordinator should wait for counter writes to complete
+counter_write_request_timeout_in_ms: 5000
+# How long a coordinator should continue to retry a CAS operation
+# that contends with other proposals for the same row
+cas_contention_timeout_in_ms: 1000
+# How long the coordinator should wait for truncates to complete
+# (This can be much longer, because unless auto_snapshot is disabled
+# we need to flush first so we can snapshot before removing the data.)
+truncate_request_timeout_in_ms: 60000
+# The default timeout for other, miscellaneous operations
+request_timeout_in_ms: 10000
+
+# How long before a node logs slow queries. Select queries that take longer than
+# this timeout to execute, will generate an aggregated log message, so that slow queries
+# can be identified. Set this value to zero to disable slow query logging.
+slow_query_log_timeout_in_ms: 500
+
+# Enable operation timeout information exchange between nodes to accurately
+# measure request timeouts. If disabled, replicas will assume that requests
+# were forwarded to them instantly by the coordinator, which means that
+# under overload conditions we will waste that much extra time processing
+# already-timed-out requests.
+#
+# Warning: before enabling this property make sure to ntp is installed
+# and the times are synchronized between the nodes.
+cross_node_timeout: false
+
+# Set keep-alive period for streaming
+# This node will send a keep-alive message periodically with this period.
+# If the node does not receive a keep-alive message from the peer for
+# 2 keep-alive cycles the stream session times out and fail
+# Default value is 300s (5 minutes), which means stalled stream
+# times out in 10 minutes by default
+# streaming_keep_alive_period_in_secs: 300
+
+# phi value that must be reached for a host to be marked down.
+# most users should never need to adjust this.
+# phi_convict_threshold: 8
+
+# endpoint_snitch -- Set this to a class that implements
+# IEndpointSnitch. The snitch has two functions:
+#
+# - it teaches Cassandra enough about your network topology to route
+# requests efficiently
+# - it allows Cassandra to spread replicas around your cluster to avoid
+# correlated failures. It does this by grouping machines into
+# "datacenters" and "racks." Cassandra will do its best not to have
+# more than one replica on the same "rack" (which may not actually
+# be a physical location)
+#
+# CASSANDRA WILL NOT ALLOW YOU TO SWITCH TO AN INCOMPATIBLE SNITCH
+# ONCE DATA IS INSERTED INTO THE CLUSTER. This would cause data loss.
+# This means that if you start with the default SimpleSnitch, which
+# locates every node on "rack1" in "datacenter1", your only options
+# if you need to add another datacenter are GossipingPropertyFileSnitch
+# (and the older PFS). From there, if you want to migrate to an
+# incompatible snitch like Ec2Snitch you can do it by adding new nodes
+# under Ec2Snitch (which will locate them in a new "datacenter") and
+# decommissioning the old ones.
+#
+# Out of the box, Cassandra provides:
+#
+# SimpleSnitch:
+# Treats Strategy order as proximity. This can improve cache
+# locality when disabling read repair. Only appropriate for
+# single-datacenter deployments.
+#
+# GossipingPropertyFileSnitch
+# This should be your go-to snitch for production use. The rack
+# and datacenter for the local node are defined in
+# cassandra-rackdc.properties and propagated to other nodes via
+# gossip. If cassandra-topology.properties exists, it is used as a
+# fallback, allowing migration from the PropertyFileSnitch.
+#
+# PropertyFileSnitch:
+# Proximity is determined by rack and data center, which are
+# explicitly configured in cassandra-topology.properties.
+#
+# Ec2Snitch:
+# Appropriate for EC2 deployments in a single Region. Loads Region
+# and Availability Zone information from the EC2 API. The Region is
+# treated as the datacenter, and the Availability Zone as the rack.
+# Only private IPs are used, so this will not work across multiple
+# Regions.
+#
+# Ec2MultiRegionSnitch:
+# Uses public IPs as broadcast_address to allow cross-region
+# connectivity. (Thus, you should set seed addresses to the public
+# IP as well.) You will need to open the storage_port or
+# ssl_storage_port on the public IP firewall. (For intra-Region
+# traffic, Cassandra will switch to the private IP after
+# establishing a connection.)
+#
+# RackInferringSnitch:
+# Proximity is determined by rack and data center, which are
+# assumed to correspond to the 3rd and 2nd octet of each node's IP
+# address, respectively. Unless this happens to match your
+# deployment conventions, this is best used as an example of
+# writing a custom Snitch class and is provided in that spirit.
+#
+# You can use a custom Snitch by setting this to the full class name
+# of the snitch, which will be assumed to be on your classpath.
+endpoint_snitch: SimpleSnitch
+
+# controls how often to perform the more expensive part of host score
+# calculation
+dynamic_snitch_update_interval_in_ms: 100
+# controls how often to reset all host scores, allowing a bad host to
+# possibly recover
+dynamic_snitch_reset_interval_in_ms: 600000
+# if set greater than zero and read_repair_chance is < 1.0, this will allow
+# 'pinning' of replicas to hosts in order to increase cache capacity.
+# The badness threshold will control how much worse the pinned host has to be
+# before the dynamic snitch will prefer other replicas over it. This is
+# expressed as a double which represents a percentage. Thus, a value of
+# 0.2 means Cassandra would continue to prefer the static snitch values
+# until the pinned host was 20% worse than the fastest.
+dynamic_snitch_badness_threshold: 0.1
+
+# request_scheduler -- Set this to a class that implements
+# RequestScheduler, which will schedule incoming client requests
+# according to the specific policy. This is useful for multi-tenancy
+# with a single Cassandra cluster.
+# NOTE: This is specifically for requests from the client and does
+# not affect inter node communication.
+# org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place
+# org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of
+# client requests to a node with a separate queue for each
+# request_scheduler_id. The scheduler is further customized by
+# request_scheduler_options as described below.
+request_scheduler: org.apache.cassandra.scheduler.NoScheduler
+
+# Scheduler Options vary based on the type of scheduler
+#
+# NoScheduler
+# Has no options
+#
+# RoundRobin
+# throttle_limit
+# The throttle_limit is the number of in-flight
+# requests per client. Requests beyond
+# that limit are queued up until
+# running requests can complete.
+# The value of 80 here is twice the number of
+# concurrent_reads + concurrent_writes.
+# default_weight
+# default_weight is optional and allows for
+# overriding the default which is 1.
+# weights
+# Weights are optional and will default to 1 or the
+# overridden default_weight. The weight translates into how
+# many requests are handled during each turn of the
+# RoundRobin, based on the scheduler id.
+#
+# request_scheduler_options:
+# throttle_limit: 80
+# default_weight: 5
+# weights:
+# Keyspace1: 1
+# Keyspace2: 5
+
+# request_scheduler_id -- An identifier based on which to perform
+# the request scheduling. Currently the only valid option is keyspace.
+# request_scheduler_id: keyspace
+
+# Enable or disable inter-node encryption
+# JVM defaults for supported SSL socket protocols and cipher suites can
+# be replaced using custom encryption options. This is not recommended
+# unless you have policies in place that dictate certain settings, or
+# need to disable vulnerable ciphers or protocols in case the JVM cannot
+# be updated.
+# FIPS compliant settings can be configured at JVM level and should not
+# involve changing encryption settings here:
+# https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/FIPS.html
+# *NOTE* No custom encryption options are enabled at the moment
+# The available internode options are : all, none, dc, rack
+#
+# If set to dc cassandra will encrypt the traffic between the DCs
+# If set to rack cassandra will encrypt the traffic between the racks
+#
+# The passwords used in these options must match the passwords used when generating
+# the keystore and truststore. For instructions on generating these files, see:
+# http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore
+#
+server_encryption_options:
+ internode_encryption: none
+ keystore: conf/keystore
+ keystore_password: cassandra
+ truststore: conf/.truststore
+ truststore_password: cassandra
+ # More advanced defaults below:
+ # protocol: TLS
+ # algorithm: SunX509
+ # store_type: JKS
+ # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA]
+ # require_client_auth: false
+ # require_endpoint_verification: false
+
+# enable or disable client/server encryption.
+client_encryption_options:
+ enabled: true
+ # If enabled and optional is set to true encrypted and unencrypted connections are handled.
+ optional: false
+ keystore: /etc/cassandra/keystore.p12
+ keystore_password: "cassandra"
+ require_client_auth: true
+ truststore: /etc/cassandra/truststore.p12
+ truststore_password: "cassandra"
+ store_type: PKCS12
+ # More advanced defaults below:
+ # protocol: TLS
+ # algorithm: SunX509
+ # store_type: JKS
+ # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA]
+
+# internode_compression controls whether traffic between nodes is
+# compressed.
+# Can be:
+#
+# all
+# all traffic is compressed
+#
+# dc
+# traffic between different datacenters is compressed
+#
+# none
+# nothing is compressed.
+internode_compression: dc
+
+# Enable or disable tcp_nodelay for inter-dc communication.
+# Disabling it will result in larger (but fewer) network packets being sent,
+# reducing overhead from the TCP protocol itself, at the cost of increasing
+# latency if you block for cross-datacenter responses.
+inter_dc_tcp_nodelay: false
+
+# TTL for different trace types used during logging of the repair process.
+tracetype_query_ttl: 86400
+tracetype_repair_ttl: 604800
+
+# By default, Cassandra logs GC Pauses greater than 200 ms at INFO level
+# This threshold can be adjusted to minimize logging if necessary
+# gc_log_threshold_in_ms: 200
+
+# If unset, all GC Pauses greater than gc_log_threshold_in_ms will log at
+# INFO level
+# UDFs (user defined functions) are disabled by default.
+# As of Cassandra 3.0 there is a sandbox in place that should prevent execution of evil code.
+enable_user_defined_functions: false
+
+# Enables scripted UDFs (JavaScript UDFs).
+# Java UDFs are always enabled, if enable_user_defined_functions is true.
+# Enable this option to be able to use UDFs with "language javascript" or any custom JSR-223 provider.
+# This option has no effect, if enable_user_defined_functions is false.
+enable_scripted_user_defined_functions: false
+
+# The default Windows kernel timer and scheduling resolution is 15.6ms for power conservation.
+# Lowering this value on Windows can provide much tighter latency and better throughput, however
+# some virtualized environments may see a negative performance impact from changing this setting
+# below their system default. The sysinternals 'clockres' tool can confirm your system's default
+# setting.
+windows_timer_interval: 1
+
+
+# Enables encrypting data at-rest (on disk). Different key providers can be plugged in, but the default reads from
+# a JCE-style keystore. A single keystore can hold multiple keys, but the one referenced by
+# the "key_alias" is the only key that will be used for encrypt opertaions; previously used keys
+# can still (and should!) be in the keystore and will be used on decrypt operations
+# (to handle the case of key rotation).
+#
+# It is strongly recommended to download and install Java Cryptography Extension (JCE)
+# Unlimited Strength Jurisdiction Policy Files for your version of the JDK.
+# (current link: http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html)
+#
+# Currently, only the following file types are supported for transparent data encryption, although
+# more are coming in future cassandra releases: commitlog, hints
+transparent_data_encryption_options:
+ enabled: false
+ chunk_length_kb: 64
+ cipher: AES/CBC/PKCS5Padding
+ key_alias: testing:1
+ # CBC IV length for AES needs to be 16 bytes (which is also the default size)
+ # iv_length: 16
+ key_provider:
+ - class_name: org.apache.cassandra.security.JKSKeyProvider
+ parameters:
+ - keystore: conf/keystore
+ keystore_password: cassandra
+ store_type: JCEKS
+ key_password: cassandra
+
+
+#####################
+# SAFETY THRESHOLDS #
+#####################
+
+# When executing a scan, within or across a partition, we need to keep the
+# tombstones seen in memory so we can return them to the coordinator, which
+# will use them to make sure other replicas also know about the deleted rows.
+# With workloads that generate a lot of tombstones, this can cause performance
+# problems and even exaust the server heap.
+# (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets)
+# Adjust the thresholds here if you understand the dangers and want to
+# scan more tombstones anyway. These thresholds may also be adjusted at runtime
+# using the StorageService mbean.
+tombstone_warn_threshold: 1000
+tombstone_failure_threshold: 100000
+
+# Log WARN on any multiple-partition batch size exceeding this value. 5kb per batch by default.
+# Caution should be taken on increasing the size of this threshold as it can lead to node instability.
+batch_size_warn_threshold_in_kb: 5
+
+# Fail any multiple-partition batch exceeding this value. 50kb (10x warn threshold) by default.
+batch_size_fail_threshold_in_kb: 50
+
+# Log WARN on any batches not of type LOGGED than span across more partitions than this limit
+unlogged_batch_across_partitions_warn_threshold: 10
+
+# Log a warning when compacting partitions larger than this value
+compaction_large_partition_warning_threshold_mb: 100
+
+# GC Pauses greater than gc_warn_threshold_in_ms will be logged at WARN level
+# Adjust the threshold based on your application throughput requirement
+# By default, Cassandra logs GC Pauses greater than 200 ms at INFO level
+gc_warn_threshold_in_ms: 1000
+
+# Maximum size of any value in SSTables. Safety measure to detect SSTable corruption
+# early. Any value size larger than this threshold will result into marking an SSTable
+# as corrupted. This should be positive and less than 2048.
+# max_value_size_in_mb: 256
+
+# Back-pressure settings #
+# If enabled, the coordinator will apply the back-pressure strategy specified below to each mutation
+# sent to replicas, with the aim of reducing pressure on overloaded replicas.
+back_pressure_enabled: false
+# The back-pressure strategy applied.
+# The default implementation, RateBasedBackPressure, takes three arguments:
+# high ratio, factor, and flow type, and uses the ratio between incoming mutation responses and outgoing mutation requests.
+# If below high ratio, outgoing mutations are rate limited according to the incoming rate decreased by the given factor;
+# if above high ratio, the rate limiting is increased by the given factor;
+# such factor is usually best configured between 1 and 10, use larger values for a faster recovery
+# at the expense of potentially more dropped mutations;
+# the rate limiting is applied according to the flow type: if FAST, it's rate limited at the speed of the fastest replica,
+# if SLOW at the speed of the slowest one.
+# New strategies can be added. Implementors need to implement org.apache.cassandra.net.BackpressureStrategy and
+# provide a public constructor accepting a Map.
+back_pressure_strategy:
+ - class_name: org.apache.cassandra.net.RateBasedBackPressure
+ parameters:
+ - high_ratio: 0.90
+ factor: 5
+ flow: FAST
+
+# Coalescing Strategies #
+# Coalescing multiples messages turns out to significantly boost message processing throughput (think doubling or more).
+# On bare metal, the floor for packet processing throughput is high enough that many applications won't notice, but in
+# virtualized environments, the point at which an application can be bound by network packet processing can be
+# surprisingly low compared to the throughput of task processing that is possible inside a VM. It's not that bare metal
+# doesn't benefit from coalescing messages, it's that the number of packets a bare metal network interface can process
+# is sufficient for many applications such that no load starvation is experienced even without coalescing.
+# There are other benefits to coalescing network messages that are harder to isolate with a simple metric like messages
+# per second. By coalescing multiple tasks together, a network thread can process multiple messages for the cost of one
+# trip to read from a socket, and all the task submission work can be done at the same time reducing context switching
+# and increasing cache friendliness of network message processing.
+# See CASSANDRA-8692 for details.
+
+# Strategy to use for coalescing messages in OutboundTcpConnection.
+# Can be fixed, movingaverage, timehorizon, disabled (default).
+# You can also specify a subclass of CoalescingStrategies.CoalescingStrategy by name.
+# otc_coalescing_strategy: DISABLED
+
+# How many microseconds to wait for coalescing. For fixed strategy this is the amount of time after the first
+# message is received before it will be sent with any accompanying messages. For moving average this is the
+# maximum amount of time that will be waited as well as the interval at which messages must arrive on average
+# for coalescing to be enabled.
+# otc_coalescing_window_us: 200
+
+# Do not try to coalesce messages if we already got that many messages. This should be more than 2 and less than 128.
+# otc_coalescing_enough_coalesced_messages: 8
+
+# How many milliseconds to wait between two expiration runs on the backlog (queue) of the OutboundTcpConnection.
+# Expiration is done if messages are piling up in the backlog. Droppable messages are expired to free the memory
+# taken by expired messages. The interval should be between 0 and 1000, and in most installations the default value
+# will be appropriate. A smaller value could potentially expire messages slightly sooner at the expense of more CPU
+# time and queue contention while iterating the backlog of messages.
+# An interval of 0 disables any wait time, which is the behavior of former Cassandra versions.
+#
+# otc_backlog_expiration_interval_ms: 200
diff --git a/modules/cassandra/src/test/resources/cassandra-ssl-configuration/keystore.p12 b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/keystore.p12
new file mode 100644
index 00000000000..501e25cf967
Binary files /dev/null and b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/keystore.p12 differ
diff --git a/modules/cassandra/src/test/resources/cassandra-ssl-configuration/truststore.p12 b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/truststore.p12
new file mode 100644
index 00000000000..57107fc3813
Binary files /dev/null and b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/truststore.p12 differ
diff --git a/modules/cassandra/src/test/resources/cassandra-test-configuration-example/cassandra.yaml b/modules/cassandra/src/test/resources/cassandra-test-configuration-example/cassandra.yaml
index 5b57b2a8e58..426dea64771 100644
--- a/modules/cassandra/src/test/resources/cassandra-test-configuration-example/cassandra.yaml
+++ b/modules/cassandra/src/test/resources/cassandra-test-configuration-example/cassandra.yaml
@@ -250,7 +250,7 @@ commit_failure_policy: stop
#
# Valid values are either "auto" (omitting the value) or a value greater 0.
#
-# Note that specifying a too large value will result in long running GCs and possbily
+# Note that specifying a too large value will result in long running GCs and possibly
# out-of-memory errors. Keep the value at a small fraction of the heap.
#
# If you constantly see "prepared statements discarded in the last minute because
@@ -259,7 +259,7 @@ commit_failure_policy: stop
# i.e. use bind markers for variable parts.
#
# Do only change the default value, if you really have more prepared statements than
-# fit in the cache. In most cases it is not neccessary to change this value.
+# fit in the cache. In most cases it is not necessary to change this value.
# Constantly re-preparing statements is a performance penalty.
#
# Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater
@@ -309,7 +309,7 @@ key_cache_save_period: 14400
# Fully off-heap row cache implementation (default).
#
# org.apache.cassandra.cache.SerializingCacheProvider
-# This is the row cache implementation availabile
+# This is the row cache implementation available
# in previous releases of Cassandra.
# row_cache_class_name: org.apache.cassandra.cache.OHCProvider
@@ -444,7 +444,7 @@ concurrent_counter_writes: 32
concurrent_materialized_view_writes: 32
# Maximum memory to use for sstable chunk cache and buffer pooling.
-# 32MB of this are reserved for pooling buffers, the rest is used as an
+# 32MB of this are reserved for pooling buffers, the rest is used as a
# cache that holds uncompressed sstable chunks.
# Defaults to the smaller of 1/4 of heap or 512MB. This pool is allocated off-heap,
# so is in addition to the memory allocated for heap. The cache also has on-heap
@@ -553,7 +553,7 @@ memtable_allocation_type: heap_buffers
# new space for cdc-tracked tables has been made available. Default to 250ms
# cdc_free_space_check_interval_ms: 250
-# A fixed memory pool size in MB for for SSTable index summaries. If left
+# A fixed memory pool size in MB for SSTable index summaries. If left
# empty, this will default to 5% of the heap size. If the memory usage of
# all index summaries exceeds this limit, SSTables with low read rates will
# shrink their index summaries in order to meet this limit. However, this
@@ -778,7 +778,7 @@ auto_snapshot: true
# number of rows per partition. The competing goals are these:
#
# - a smaller granularity means more index entries are generated
-# and looking up rows withing the partition by collation column
+# and looking up rows within the partition by collation column
# is faster
# - but, Cassandra will keep the collation index in memory for hot
# rows (as part of the key cache), so a larger granularity means
@@ -1109,7 +1109,7 @@ windows_timer_interval: 1
# Enables encrypting data at-rest (on disk). Different key providers can be plugged in, but the default reads from
# a JCE-style keystore. A single keystore can hold multiple keys, but the one referenced by
-# the "key_alias" is the only key that will be used for encrypt opertaions; previously used keys
+# the "key_alias" is the only key that will be used for encrypt operations; previously used keys
# can still (and should!) be in the keystore and will be used on decrypt operations
# (to handle the case of key rotation).
#
@@ -1143,7 +1143,7 @@ transparent_data_encryption_options:
# tombstones seen in memory so we can return them to the coordinator, which
# will use them to make sure other replicas also know about the deleted rows.
# With workloads that generate a lot of tombstones, this can cause performance
-# problems and even exaust the server heap.
+# problems and even exhaust the server heap.
# (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets)
# Adjust the thresholds here if you understand the dangers and want to
# scan more tombstones anyway. These thresholds may also be adjusted at runtime
diff --git a/modules/cassandra/src/test/resources/client-ssl/cassandra.cer.pem b/modules/cassandra/src/test/resources/client-ssl/cassandra.cer.pem
new file mode 100644
index 00000000000..bafaa00317b
--- /dev/null
+++ b/modules/cassandra/src/test/resources/client-ssl/cassandra.cer.pem
@@ -0,0 +1,26 @@
+Bag Attributes
+ friendlyName: localhost
+ localKeyID: 54 69 6D 65 20 31 37 32 39 33 34 38 39 36 38 31 31 39
+subject=C = None, L = None, O = Testcontainers, OU = Testcontainers, CN = localhost
+issuer=C = None, L = None, O = Testcontainers, OU = Testcontainers, CN = localhost
+-----BEGIN CERTIFICATE-----
+MIIDbjCCAlagAwIBAgIJAKCVIipuH03/MA0GCSqGSIb3DQEBCwUAMGQxDTALBgNV
+BAYTBE5vbmUxDTALBgNVBAcTBE5vbmUxFzAVBgNVBAoTDlRlc3Rjb250YWluZXJz
+MRcwFQYDVQQLEw5UZXN0Y29udGFpbmVyczESMBAGA1UEAxMJbG9jYWxob3N0MCAX
+DTI0MTAxOTE0NDIwOFoYDzIxMjQwOTI1MTQ0MjA4WjBkMQ0wCwYDVQQGEwROb25l
+MQ0wCwYDVQQHEwROb25lMRcwFQYDVQQKEw5UZXN0Y29udGFpbmVyczEXMBUGA1UE
+CxMOVGVzdGNvbnRhaW5lcnMxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBALocrhrM1gYB/pF/qlDY+eFQZ9L8SMgCmn+I
+mgx/UbKqJwLp5wYuoW/PA4RwraFPkimf5CAE2kpBGcJu/Qzyp0fJZlBXpmkDJVrG
+pRbYz5mN4CrXNliYfAC1RzxvTT1tOjiDkk9kHVfs5nMVb9e2kq6tQEItflhlPzdD
+FOe0pY2XBX2stcQ6URRkK5buyPeLhnTrKMfLWEWKKKzSQGen+lbtBURZzkpmK88q
+qjLqqaZusXP6QlRVLqMADjQf7aXLi0A/fIhVrq1amqqiApJbijT0LP48DvS8DQQL
+jNKkQ17vMClMmXusU5IgJMlXfGEzeTNUI56wHGYUdE69FTGFvZECAwEAAaMhMB8w
+HQYDVR0OBBYEFNsvIE+IgkE0aTc+1MI7hpPQL2ZEMA0GCSqGSIb3DQEBCwUAA4IB
+AQC4U/tGPuRS3m/r1p3aAq0D88UGg6oKHwqe3re3xrFAv9y+Y3M+FXyh5w/yMCAr
+PcVo6Pef3hEjwc9wDuQoIcQ9eRZtYI1RnhkkuC8TZRk1KGKg9Lj4Zzbse7FfK92Z
+DUYgIVyhC/YkeEDwTiZI8WxhbglozNg5Ygw+qLK4rYmk+X/NgdfdQHocuJ3Jwqqx
+eYz0m2RUMhzxEI2z9jQr3DgjNkYrphLzaVXmO4MovzXx3DNeC8ADot9PGmaz24rl
+RDeSWynxbgqzdXGHxtyR0LY1k+Y+5wqU28L90D0o3ZtaMBnK+Ft2AP2zpbtgr8rR
+sf12uPyRUPzJQ46KNpjy4HN6
+-----END CERTIFICATE-----
diff --git a/modules/cassandra/src/test/resources/client-ssl/cassandra.key.pem b/modules/cassandra/src/test/resources/client-ssl/cassandra.key.pem
new file mode 100644
index 00000000000..4d291cf8852
--- /dev/null
+++ b/modules/cassandra/src/test/resources/client-ssl/cassandra.key.pem
@@ -0,0 +1,32 @@
+Bag Attributes
+ friendlyName: localhost
+ localKeyID: 54 69 6D 65 20 31 37 32 39 33 34 38 39 36 38 31 31 39
+Key Attributes:
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC6HK4azNYGAf6R
+f6pQ2PnhUGfS/EjIApp/iJoMf1GyqicC6ecGLqFvzwOEcK2hT5Ipn+QgBNpKQRnC
+bv0M8qdHyWZQV6ZpAyVaxqUW2M+ZjeAq1zZYmHwAtUc8b009bTo4g5JPZB1X7OZz
+FW/XtpKurUBCLX5YZT83QxTntKWNlwV9rLXEOlEUZCuW7sj3i4Z06yjHy1hFiiis
+0kBnp/pW7QVEWc5KZivPKqoy6qmmbrFz+kJUVS6jAA40H+2ly4tAP3yIVa6tWpqq
+ogKSW4o09Cz+PA70vA0EC4zSpENe7zApTJl7rFOSICTJV3xhM3kzVCOesBxmFHRO
+vRUxhb2RAgMBAAECggEACqD5e7C+Rr8oz+jS/z8FAxsmcsqgFXW6NEjG4EPWx89a
+RWfthVFDov3XNsizzp/OulXWH2xnhkOyU7cm+Ia7JI+Z9w8Qz+dM5AkVA8Y23o9X
+TnSjNx57DODnEP21eZAzxpp50DlPFU02pzsbYhE2AbsFp0HirB9MI70CN24xR9hP
+i1zPgO7FVnLvn4INqVKgcV4vXlxvgDEvO4Myc1WoJXkyPCCObvEflBBWr2QwQfKT
+T2qjCJWv/P2PJGFaZbOrEvOHZjprSid4/n9gbQrodGoChhjiZT//l19Ay+7eJkUc
+yiiSK4u3fF4YPH9+CVpRQ94PHFe+0kQVvf+VGX/iqQKBgQDp5umTcZoho/KQzOd/
+pA8fgnzbipEl5ep2MHB98cqEQ93eFv4l/bBehDQ1WvmFJY8SIzU4EQotpnCZd4Vb
+KH9PE8tsTRvw2cYbBuD751boLVaBn8wxtlTkFrN/CyAtV7w7AG0dXnDKushJx1NN
+8AgvSr0X4hf0AKIWGtVteoX7qwKBgQDLse7Ze5dNbG48CpBGqQS4wFkTSEs5QKI5
+68JXEQoCmJb06O7rxj4f5CALv3pReP3nrl5+kLmT8O+yU5C5dXf06k/z4GDJ6Esm
+8XTEfB2Ca+kI2RLyMRRXPA2nEunbSsyk1AVo2GeRJxG4TaDbu2zTkUBAEyCuwarW
+OMsuYodPswKBgQCZc/kB1qH8OAdHoGawgv24+m7Xycz4RCLSb20d86edprjEn+kV
+G56+I5Xs+0aAZ+e5Sof7xJIc6Pkudg9zgtojEyV+ZAhUt0sVKCoqmdeWc0gxupjI
+dIq1KX+RdccieFDxlJIBlpgBKRGF9dNdaoC0JiBwrtBwMIomXmxvatbECQKBgQCS
+X1xZn/xLwJ0+PAENJauk72OS/aJAk/d/U7ElS7M7xlbDyxbVCnHeDNoSVxgYr68U
+6zIwFOOmMb6tEGuxOX5n2nB1uUkUDf7jDyNvhhjWfaDJoOOCck5BmX/eDTNLR+bi
+kxEIFGnn3oFXRUFQZNCA/6GB6bzUl4qhwdIPlPHTDQKBgQCgztlUF5IOJFKMjVlY
+yoA/7+b5zwrh8Y2+SLzF/HLah85AuHxsgdTuQh+HLKSwJejKCT95BSRJO/kV0XCR
+KZGStqETpEH/2AJkxpjt0FZxtIQdnyTargbiipe4JzI3iCTLtfN5C9Pn3ZJ9giap
+B5uQm4762aH2jw1kKFegHlIgJg==
+-----END PRIVATE KEY-----
diff --git a/modules/cassandra/src/test/resources/initial-with-error.cql b/modules/cassandra/src/test/resources/initial-with-error.cql
new file mode 100644
index 00000000000..4f0e11d721b
--- /dev/null
+++ b/modules/cassandra/src/test/resources/initial-with-error.cql
@@ -0,0 +1,6 @@
+CREATE KEYSPACE keySpaceTest WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};
+
+USE keySpaceTest;
+
+/* The following statement contains an error (missing primary key) on purpose, do not fix it! */
+CREATE TABLE catalog_category (id bigint);
diff --git a/modules/cassandra/src/test/resources/logback-test.xml b/modules/cassandra/src/test/resources/logback-test.xml
index 535e406fc13..83ef7a1a3ef 100644
--- a/modules/cassandra/src/test/resources/logback-test.xml
+++ b/modules/cassandra/src/test/resources/logback-test.xml
@@ -12,5 +12,5 @@
-
+
diff --git a/modules/chromadb/build.gradle b/modules/chromadb/build.gradle
new file mode 100644
index 00000000000..ec41def56df
--- /dev/null
+++ b/modules/chromadb/build.gradle
@@ -0,0 +1,7 @@
+description = "Testcontainers :: ChromaDB"
+
+dependencies {
+ api project(':testcontainers')
+
+ testImplementation 'io.rest-assured:rest-assured:5.5.7'
+}
diff --git a/modules/chromadb/src/main/java/org/testcontainers/chromadb/ChromaDBContainer.java b/modules/chromadb/src/main/java/org/testcontainers/chromadb/ChromaDBContainer.java
new file mode 100644
index 00000000000..af6c3df33fc
--- /dev/null
+++ b/modules/chromadb/src/main/java/org/testcontainers/chromadb/ChromaDBContainer.java
@@ -0,0 +1,56 @@
+package org.testcontainers.chromadb;
+
+import lombok.extern.slf4j.Slf4j;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.ComparableVersion;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Testcontainers implementation of ChromaDB.
+ *
+ * Supported images: {@code chromadb/chroma}, {@code ghcr.io/chroma-core/chroma}
+ *
+ * Exposed ports: 8000
+ */
+@Slf4j
+public class ChromaDBContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_DOCKER_IMAGE = DockerImageName.parse("chromadb/chroma");
+
+ private static final DockerImageName GHCR_DOCKER_IMAGE = DockerImageName.parse("ghcr.io/chroma-core/chroma");
+
+ public ChromaDBContainer(String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ public ChromaDBContainer(DockerImageName dockerImageName) {
+ this(dockerImageName, isVersion2(dockerImageName.getVersionPart()));
+ }
+
+ public ChromaDBContainer(DockerImageName dockerImageName, boolean isVersion2) {
+ super(dockerImageName);
+ String apiPath = isVersion2 ? "/api/v2/heartbeat" : "/api/v1/heartbeat";
+ dockerImageName.assertCompatibleWith(DEFAULT_DOCKER_IMAGE, GHCR_DOCKER_IMAGE);
+ withExposedPorts(8000);
+ waitingFor(Wait.forHttp(apiPath));
+ }
+
+ public String getEndpoint() {
+ return "http://" + getHost() + ":" + getFirstMappedPort();
+ }
+
+ private static boolean isVersion2(String version) {
+ if (version.equals("latest")) {
+ return true;
+ }
+
+ ComparableVersion comparableVersion = new ComparableVersion(version);
+ if (comparableVersion.isGreaterThanOrEqualTo("1.0.0")) {
+ return true;
+ }
+
+ log.warn("Version {} is less than 1.0.0 or not a semantic version.", version);
+ return false;
+ }
+}
diff --git a/modules/chromadb/src/test/java/org/testcontainers/chromadb/ChromaDBContainerTest.java b/modules/chromadb/src/test/java/org/testcontainers/chromadb/ChromaDBContainerTest.java
new file mode 100644
index 00000000000..579c75f9f36
--- /dev/null
+++ b/modules/chromadb/src/test/java/org/testcontainers/chromadb/ChromaDBContainerTest.java
@@ -0,0 +1,48 @@
+package org.testcontainers.chromadb;
+
+import io.restassured.http.ContentType;
+import org.junit.jupiter.api.Test;
+
+import static io.restassured.RestAssured.given;
+
+class ChromaDBContainerTest {
+
+ @Test
+ void test() {
+ try ( // container {
+ ChromaDBContainer chroma = new ChromaDBContainer("chromadb/chroma:0.4.23")
+ // }
+ ) {
+ chroma.start();
+
+ given()
+ .baseUri(chroma.getEndpoint())
+ .when()
+ .body("{\"name\": \"test\"}")
+ .contentType(ContentType.JSON)
+ .post("/api/v1/databases")
+ .then()
+ .statusCode(200);
+
+ given().baseUri(chroma.getEndpoint()).when().get("/api/v1/databases/test").then().statusCode(200);
+ }
+ }
+
+ @Test
+ void testVersion2() {
+ try (ChromaDBContainer chroma = new ChromaDBContainer("chromadb/chroma:1.0.0")) {
+ chroma.start();
+
+ given()
+ .baseUri(chroma.getEndpoint())
+ .when()
+ .body("{\"name\": \"test\"}")
+ .contentType(ContentType.JSON)
+ .post("/api/v2/tenants")
+ .then()
+ .statusCode(200);
+
+ given().baseUri(chroma.getEndpoint()).when().get("/api/v2/tenants/test").then().statusCode(200);
+ }
+ }
+}
diff --git a/modules/chromadb/src/test/resources/logback-test.xml b/modules/chromadb/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..83ef7a1a3ef
--- /dev/null
+++ b/modules/chromadb/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/modules/clickhouse/build.gradle b/modules/clickhouse/build.gradle
index 9f7df373b27..59ea169583c 100644
--- a/modules/clickhouse/build.gradle
+++ b/modules/clickhouse/build.gradle
@@ -2,9 +2,15 @@ description = "Testcontainers :: JDBC :: ClickHouse"
dependencies {
api project(':testcontainers')
- api project(':jdbc')
+ api project(':testcontainers-jdbc')
- testImplementation project(':jdbc-test')
- testImplementation 'ru.yandex.clickhouse:clickhouse-jdbc:0.3.2'
- testImplementation 'org.assertj:assertj-core:3.23.1'
+ compileOnly project(':testcontainers-r2dbc')
+ compileOnly(group: 'com.clickhouse', name: 'clickhouse-r2dbc', version: '0.9.8', classifier: 'http')
+
+ testImplementation project(':testcontainers-jdbc-test')
+ testRuntimeOnly(group: 'com.clickhouse', name: 'clickhouse-jdbc', version: '0.9.8', classifier: 'all')
+
+ testImplementation 'com.clickhouse:client-v2:0.9.8'
+ testImplementation testFixtures(project(':testcontainers-r2dbc'))
+ testRuntimeOnly(group: 'com.clickhouse', name: 'clickhouse-r2dbc', version: '0.9.8', classifier: 'http')
}
diff --git a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java
new file mode 100644
index 00000000000..02a02267ec5
--- /dev/null
+++ b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java
@@ -0,0 +1,150 @@
+package org.testcontainers.clickhouse;
+
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.time.Duration;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Testcontainers implementation for ClickHouse.
+ *
+ * Supported image: {@code clickhouse/clickhouse-server}
+ *
+ * Exposed ports:
+ *
+ * Database: 8123
+ * Console: 9000
+ *
+ */
+public class ClickHouseContainer extends JdbcDatabaseContainer {
+
+ static final String CLICKHOUSE_CLICKHOUSE_SERVER = "clickhouse/clickhouse-server";
+
+ private static final DockerImageName CLICKHOUSE_IMAGE_NAME = DockerImageName.parse(CLICKHOUSE_CLICKHOUSE_SERVER);
+
+ static final Integer HTTP_PORT = 8123;
+
+ static final Integer NATIVE_PORT = 9000;
+
+ private static final String LEGACY_V1_DRIVER_CLASS_NAME = "com.clickhouse.jdbc.ClickHouseDriver";
+
+ private static final String DRIVER_CLASS_NAME = "com.clickhouse.jdbc.Driver";
+
+ private static final String JDBC_URL_PREFIX = "jdbc:clickhouse://";
+
+ private static final String TEST_QUERY = "SELECT 1";
+
+ static final String DEFAULT_USER = "test";
+
+ static final String DEFAULT_PASSWORD = "test";
+
+ private String databaseName = "default";
+
+ private String username = DEFAULT_USER;
+
+ private String password = DEFAULT_PASSWORD;
+
+ public ClickHouseContainer(String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ public ClickHouseContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(CLICKHOUSE_IMAGE_NAME);
+
+ addExposedPorts(HTTP_PORT, NATIVE_PORT);
+ waitingFor(
+ Wait
+ .forHttp("/")
+ .forPort(HTTP_PORT)
+ .forStatusCode(200)
+ .forResponsePredicate("Ok."::equals)
+ .withStartupTimeout(Duration.ofMinutes(1))
+ );
+ }
+
+ @Override
+ protected void configure() {
+ withEnv("CLICKHOUSE_DB", this.databaseName);
+ withEnv("CLICKHOUSE_USER", this.username);
+ withEnv("CLICKHOUSE_PASSWORD", this.password);
+ }
+
+ @Override
+ public Set getLivenessCheckPortNumbers() {
+ return new HashSet<>(getMappedPort(HTTP_PORT));
+ }
+
+ @Override
+ public String getDriverClassName() {
+ try {
+ Class.forName(DRIVER_CLASS_NAME);
+ return DRIVER_CLASS_NAME;
+ } catch (ClassNotFoundException e) {
+ return LEGACY_V1_DRIVER_CLASS_NAME;
+ }
+ }
+
+ @Override
+ public String getJdbcUrl() {
+ return (
+ JDBC_URL_PREFIX +
+ getHost() +
+ ":" +
+ getMappedPort(HTTP_PORT) +
+ "/" +
+ this.databaseName +
+ constructUrlParameters("?", "&")
+ );
+ }
+
+ public String getHttpUrl() {
+ return "http://" + getHost() + ":" + getMappedPort(HTTP_PORT);
+ }
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String getDatabaseName() {
+ return databaseName;
+ }
+
+ @Override
+ public String getTestQueryString() {
+ return TEST_QUERY;
+ }
+
+ @Override
+ public ClickHouseContainer withUsername(String username) {
+ this.username = username;
+ return this;
+ }
+
+ @Override
+ public ClickHouseContainer withPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+ @Override
+ public ClickHouseContainer withDatabaseName(String databaseName) {
+ this.databaseName = databaseName;
+ return this;
+ }
+
+ @Override
+ protected void waitUntilContainerStarted() {
+ getWaitStrategy().waitUntilReady(this);
+ }
+}
diff --git a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java
new file mode 100644
index 00000000000..be6d8af67c2
--- /dev/null
+++ b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java
@@ -0,0 +1,48 @@
+package org.testcontainers.clickhouse;
+
+import io.r2dbc.spi.ConnectionFactoryOptions;
+import org.testcontainers.r2dbc.R2DBCDatabaseContainer;
+
+/**
+ * ClickHouse R2DBC support
+ */
+public class ClickHouseR2DBCDatabaseContainer implements R2DBCDatabaseContainer {
+
+ private final ClickHouseContainer container;
+
+ public ClickHouseR2DBCDatabaseContainer(ClickHouseContainer container) {
+ this.container = container;
+ }
+
+ public static ConnectionFactoryOptions getOptions(ClickHouseContainer container) {
+ ConnectionFactoryOptions options = ConnectionFactoryOptions
+ .builder()
+ .option(ConnectionFactoryOptions.DRIVER, ClickHouseR2DBCDatabaseContainerProvider.DRIVER)
+ .build();
+
+ return new ClickHouseR2DBCDatabaseContainer(container).configure(options);
+ }
+
+ @Override
+ public void start() {
+ this.container.start();
+ }
+
+ @Override
+ public void stop() {
+ this.container.stop();
+ }
+
+ @Override
+ public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) {
+ return options
+ .mutate()
+ .option(ConnectionFactoryOptions.HOST, container.getHost())
+ .option(ConnectionFactoryOptions.PORT, container.getMappedPort(ClickHouseContainer.HTTP_PORT))
+ .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName())
+ .option(ConnectionFactoryOptions.USER, container.getUsername())
+ .option(ConnectionFactoryOptions.PASSWORD, container.getPassword())
+ .option(ConnectionFactoryOptions.PROTOCOL, "http")
+ .build();
+ }
+}
diff --git a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerProvider.java b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerProvider.java
new file mode 100644
index 00000000000..d2005f15bb1
--- /dev/null
+++ b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerProvider.java
@@ -0,0 +1,46 @@
+package org.testcontainers.clickhouse;
+
+import com.clickhouse.r2dbc.connection.ClickHouseConnectionFactoryProvider;
+import io.r2dbc.spi.ConnectionFactoryMetadata;
+import io.r2dbc.spi.ConnectionFactoryOptions;
+import org.testcontainers.r2dbc.R2DBCDatabaseContainer;
+import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider;
+
+import javax.annotation.Nullable;
+
+public class ClickHouseR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider {
+
+ static final String DRIVER = ClickHouseConnectionFactoryProvider.CLICKHOUSE_DRIVER;
+
+ @Override
+ public boolean supports(ConnectionFactoryOptions options) {
+ return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER));
+ }
+
+ @Override
+ public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) {
+ String image =
+ ClickHouseContainer.CLICKHOUSE_CLICKHOUSE_SERVER + ":" + options.getRequiredValue(IMAGE_TAG_OPTION);
+ ClickHouseContainer container = new ClickHouseContainer(image)
+ .withDatabaseName((String) options.getRequiredValue(ConnectionFactoryOptions.DATABASE));
+
+ if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) {
+ container.withReuse(true);
+ }
+ return new ClickHouseR2DBCDatabaseContainer(container);
+ }
+
+ @Nullable
+ @Override
+ public ConnectionFactoryMetadata getMetadata(ConnectionFactoryOptions options) {
+ ConnectionFactoryOptions.Builder builder = options.mutate();
+ if (!options.hasOption(ConnectionFactoryOptions.USER)) {
+ builder.option(ConnectionFactoryOptions.USER, ClickHouseContainer.DEFAULT_USER);
+ }
+ if (!options.hasOption(ConnectionFactoryOptions.PASSWORD)) {
+ builder.option(ConnectionFactoryOptions.PASSWORD, ClickHouseContainer.DEFAULT_PASSWORD);
+ }
+ builder.option(ConnectionFactoryOptions.PROTOCOL, "http");
+ return R2DBCDatabaseContainerProvider.super.getMetadata(builder.build());
+ }
+}
diff --git a/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseContainer.java b/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseContainer.java
index 4e87c77ee5b..e9f10feb746 100644
--- a/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseContainer.java
+++ b/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseContainer.java
@@ -1,12 +1,18 @@
package org.testcontainers.containers;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
+import org.testcontainers.utility.ComparableVersion;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
+/**
+ * Testcontainers implementation for ClickHouse.
+ *
+ * @deprecated use {@link org.testcontainers.clickhouse.ClickHouseContainer} instead
+ */
public class ClickHouseContainer extends JdbcDatabaseContainer {
public static final String NAME = "clickhouse";
@@ -25,7 +31,9 @@ public class ClickHouseContainer extends JdbcDatabaseContainer getLivenessCheckPortNumbers() {
@Override
public String getDriverClassName() {
try {
- Class.forName(DRIVER_CLASS_NAME);
- return DRIVER_CLASS_NAME;
+ if (supportsNewDriver) {
+ Class.forName(DRIVER_CLASS_NAME);
+ return DRIVER_CLASS_NAME;
+ } else {
+ return LEGACY_DRIVER_CLASS_NAME;
+ }
} catch (ClassNotFoundException e) {
- return "com.clickhouse.jdbc.ClickHouseDriver";
+ return LEGACY_DRIVER_CLASS_NAME;
}
}
+ private static boolean isNewDriverSupported(DockerImageName dockerImageName) {
+ // New driver supports versions 20.7+. Check the version part of the tag
+ return new ComparableVersion(dockerImageName.getVersionPart()).isGreaterThanOrEqualTo("20.7");
+ }
+
@Override
public String getJdbcUrl() {
return JDBC_URL_PREFIX + getHost() + ":" + getMappedPort(HTTP_PORT) + "/" + databaseName;
diff --git a/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseProvider.java b/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseProvider.java
index 80fb71bd5da..250631c1500 100644
--- a/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseProvider.java
+++ b/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseProvider.java
@@ -1,16 +1,24 @@
package org.testcontainers.containers;
+import org.testcontainers.clickhouse.ClickHouseContainer;
import org.testcontainers.utility.DockerImageName;
public class ClickHouseProvider extends JdbcDatabaseContainerProvider {
+ private static final String DEFAULT_TAG = "24.12-alpine";
+
@Override
public boolean supports(String databaseType) {
- return databaseType.equals(ClickHouseContainer.NAME);
+ return databaseType.equals("clickhouse");
+ }
+
+ @Override
+ public JdbcDatabaseContainer> newInstance() {
+ return newInstance(DEFAULT_TAG);
}
@Override
- public JdbcDatabaseContainer newInstance(String tag) {
- return new ClickHouseContainer(DockerImageName.parse(ClickHouseContainer.IMAGE).withTag(tag));
+ public JdbcDatabaseContainer> newInstance(String tag) {
+ return new ClickHouseContainer(DockerImageName.parse("clickhouse/clickhouse-server").withTag(tag));
}
}
diff --git a/modules/clickhouse/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/clickhouse/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider
new file mode 100644
index 00000000000..4898be97d99
--- /dev/null
+++ b/modules/clickhouse/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider
@@ -0,0 +1 @@
+org.testcontainers.clickhouse.ClickHouseR2DBCDatabaseContainerProvider
diff --git a/modules/clickhouse/src/test/java/org/testcontainers/ClickhouseTestImages.java b/modules/clickhouse/src/test/java/org/testcontainers/ClickhouseTestImages.java
index a6707705ff8..eff4e19f70b 100644
--- a/modules/clickhouse/src/test/java/org/testcontainers/ClickhouseTestImages.java
+++ b/modules/clickhouse/src/test/java/org/testcontainers/ClickhouseTestImages.java
@@ -3,6 +3,7 @@
import org.testcontainers.utility.DockerImageName;
public interface ClickhouseTestImages {
- DockerImageName YANDEX_CLICKHOUSE_IMAGE = DockerImageName.parse("yandex/clickhouse-server:18.10.3");
- DockerImageName CLICKHOUSE_IMAGE = DockerImageName.parse("clickhouse/clickhouse-server:21.9.2-alpine");
+ DockerImageName CLICKHOUSE_IMAGE = DockerImageName.parse("clickhouse/clickhouse-server:21.11.11-alpine");
+
+ DockerImageName CLICKHOUSE_24_12_IMAGE = DockerImageName.parse("clickhouse/clickhouse-server:24.12-alpine");
}
diff --git a/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseContainerTest.java b/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseContainerTest.java
new file mode 100644
index 00000000000..0f9eb3ef6a3
--- /dev/null
+++ b/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseContainerTest.java
@@ -0,0 +1,92 @@
+package org.testcontainers.clickhouse;
+
+import com.clickhouse.client.api.Client;
+import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader;
+import com.clickhouse.client.api.query.QueryResponse;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.ClickhouseTestImages;
+import org.testcontainers.db.AbstractContainerDatabaseTest;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+class ClickHouseContainerTest extends AbstractContainerDatabaseTest {
+
+ @Test
+ void testSimple() throws SQLException {
+ try ( // container {
+ ClickHouseContainer clickhouse = new ClickHouseContainer("clickhouse/clickhouse-server:21.11-alpine")
+ // }
+ ) {
+ clickhouse.start();
+
+ ResultSet resultSet = performQuery(clickhouse, "SELECT 1");
+
+ int resultSetInt = resultSet.getInt(1);
+ assertThat(resultSetInt).isEqualTo(1);
+ }
+ }
+
+ @Test
+ void customCredentialsWithUrlParams() throws SQLException {
+ try (
+ ClickHouseContainer clickhouse = new ClickHouseContainer("clickhouse/clickhouse-server:21.11.2-alpine")
+ .withUsername("default")
+ .withPassword("")
+ .withDatabaseName("test")
+ // The new driver uses the prefix `clickhouse_setting_` for session settings
+ .withUrlParam("clickhouse_setting_max_result_rows", "5")
+ ) {
+ clickhouse.start();
+
+ ResultSet resultSet = performQuery(
+ clickhouse,
+ "SELECT value FROM system.settings where name='max_result_rows'"
+ );
+
+ int resultSetInt = resultSet.getInt(1);
+ assertThat(resultSetInt).isEqualTo(5);
+ }
+ }
+
+ @Test
+ void testNewAuth() throws SQLException {
+ try (ClickHouseContainer clickhouse = new ClickHouseContainer(ClickhouseTestImages.CLICKHOUSE_24_12_IMAGE)) {
+ clickhouse.start();
+
+ ResultSet resultSet = performQuery(clickhouse, "SELECT 1");
+
+ int resultSetInt = resultSet.getInt(1);
+ assertThat(resultSetInt).isEqualTo(1);
+ }
+ }
+
+ @Test
+ void testGetHttpMethodWithHttpClient() {
+ ClickHouseContainer clickhouse = new ClickHouseContainer(ClickhouseTestImages.CLICKHOUSE_24_12_IMAGE);
+ clickhouse.start();
+ Client client = new Client.Builder()
+ .addEndpoint(clickhouse.getHttpUrl())
+ .setUsername(clickhouse.getUsername())
+ .setPassword(clickhouse.getPassword())
+ .build();
+ try {
+ QueryResponse queryResponse = client.query("SELECT 1").get(1, TimeUnit.MINUTES);
+ ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(queryResponse);
+ reader.next();
+ int result = reader.getInteger(1);
+ assertThat(result).isEqualTo(1);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ fail("Cannot get sql result:" + e);
+ } finally {
+ clickhouse.close();
+ client.close();
+ }
+ }
+}
diff --git a/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerTest.java b/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerTest.java
new file mode 100644
index 00000000000..000fff8983a
--- /dev/null
+++ b/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerTest.java
@@ -0,0 +1,22 @@
+package org.testcontainers.clickhouse;
+
+import io.r2dbc.spi.ConnectionFactoryOptions;
+import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest;
+
+public class ClickHouseR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest {
+
+ @Override
+ protected ConnectionFactoryOptions getOptions(ClickHouseContainer container) {
+ return ClickHouseR2DBCDatabaseContainer.getOptions(container);
+ }
+
+ @Override
+ protected String createR2DBCUrl() {
+ return "r2dbc:tc:clickhouse:///db?TC_IMAGE_TAG=21.11.11-alpine";
+ }
+
+ @Override
+ protected ClickHouseContainer createContainer() {
+ return new ClickHouseContainer("clickhouse/clickhouse-server:21.11.11-alpine");
+ }
+}
diff --git a/modules/clickhouse/src/test/java/org/testcontainers/jdbc/clickhouse/ClickhouseJDBCDriverTest.java b/modules/clickhouse/src/test/java/org/testcontainers/jdbc/clickhouse/ClickhouseJDBCDriverTest.java
index 056aace7772..0a85a26899a 100644
--- a/modules/clickhouse/src/test/java/org/testcontainers/jdbc/clickhouse/ClickhouseJDBCDriverTest.java
+++ b/modules/clickhouse/src/test/java/org/testcontainers/jdbc/clickhouse/ClickhouseJDBCDriverTest.java
@@ -1,16 +1,12 @@
package org.testcontainers.jdbc.clickhouse;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
import org.testcontainers.jdbc.AbstractJDBCDriverTest;
import java.util.Arrays;
import java.util.EnumSet;
-@RunWith(Parameterized.class)
-public class ClickhouseJDBCDriverTest extends AbstractJDBCDriverTest {
+class ClickhouseJDBCDriverTest extends AbstractJDBCDriverTest {
- @Parameterized.Parameters(name = "{index} - {0}")
public static Iterable data() {
return Arrays.asList(
new Object[][] { //
diff --git a/modules/clickhouse/src/test/java/org/testcontainers/junit/clickhouse/SimpleClickhouseTest.java b/modules/clickhouse/src/test/java/org/testcontainers/junit/clickhouse/SimpleClickhouseTest.java
index 8f81c88bfc0..c9bd9e917f5 100644
--- a/modules/clickhouse/src/test/java/org/testcontainers/junit/clickhouse/SimpleClickhouseTest.java
+++ b/modules/clickhouse/src/test/java/org/testcontainers/junit/clickhouse/SimpleClickhouseTest.java
@@ -1,38 +1,20 @@
package org.testcontainers.junit.clickhouse;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
+import org.junit.jupiter.api.Test;
import org.testcontainers.ClickhouseTestImages;
import org.testcontainers.containers.ClickHouseContainer;
import org.testcontainers.db.AbstractContainerDatabaseTest;
-import org.testcontainers.utility.DockerImageName;
import java.sql.ResultSet;
import java.sql.SQLException;
import static org.assertj.core.api.Assertions.assertThat;
-@RunWith(Parameterized.class)
-public class SimpleClickhouseTest extends AbstractContainerDatabaseTest {
-
- private final DockerImageName imageName;
-
- public SimpleClickhouseTest(DockerImageName imageName) {
- this.imageName = imageName;
- }
-
- @Parameterized.Parameters(name = "{0}")
- public static Object[][] data() {
- return new Object[][] { //
- { ClickhouseTestImages.CLICKHOUSE_IMAGE },
- { ClickhouseTestImages.YANDEX_CLICKHOUSE_IMAGE },
- };
- }
+class SimpleClickhouseTest extends AbstractContainerDatabaseTest {
@Test
public void testSimple() throws SQLException {
- try (ClickHouseContainer clickhouse = new ClickHouseContainer(this.imageName)) {
+ try (ClickHouseContainer clickhouse = new ClickHouseContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) {
clickhouse.start();
ResultSet resultSet = performQuery(clickhouse, "SELECT 1");
diff --git a/modules/clickhouse/src/test/resources/logback-test.xml b/modules/clickhouse/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..83ef7a1a3ef
--- /dev/null
+++ b/modules/clickhouse/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/modules/cockroachdb/build.gradle b/modules/cockroachdb/build.gradle
index 638629ef22a..275e69eef3d 100644
--- a/modules/cockroachdb/build.gradle
+++ b/modules/cockroachdb/build.gradle
@@ -1,9 +1,9 @@
description = "Testcontainers :: JDBC :: CockroachDB"
dependencies {
- api project(':jdbc')
+ api project(':testcontainers-jdbc')
- testImplementation project(':jdbc-test')
- testImplementation 'org.postgresql:postgresql:42.5.0'
- testImplementation 'org.assertj:assertj-core:3.23.1'
+ testRuntimeOnly 'org.postgresql:postgresql:42.7.10'
+
+ testImplementation project(':testcontainers-jdbc-test')
}
diff --git a/modules/cockroachdb/src/main/java/org/testcontainers/cockroachdb/CockroachContainer.java b/modules/cockroachdb/src/main/java/org/testcontainers/cockroachdb/CockroachContainer.java
new file mode 100644
index 00000000000..0d49e8cdaf7
--- /dev/null
+++ b/modules/cockroachdb/src/main/java/org/testcontainers/cockroachdb/CockroachContainer.java
@@ -0,0 +1,158 @@
+package org.testcontainers.cockroachdb;
+
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
+import org.testcontainers.utility.ComparableVersion;
+import org.testcontainers.utility.DockerImageName;
+
+import java.time.Duration;
+
+/**
+ * Testcontainers implementation for CockroachDB.
+ *
+ * Supported image: {@code cockroachdb/cockroach}
+ *
+ * Exposed ports:
+ *
+ * Database: 26257
+ * Console: 8080
+ *
+ */
+public class CockroachContainer extends JdbcDatabaseContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("cockroachdb/cockroach");
+
+ public static final String NAME = "cockroach";
+
+ private static final String JDBC_DRIVER_CLASS_NAME = "org.postgresql.Driver";
+
+ private static final String JDBC_URL_PREFIX = "jdbc:postgresql";
+
+ private static final String TEST_QUERY_STRING = "SELECT 1";
+
+ private static final int REST_API_PORT = 8080;
+
+ private static final int DB_PORT = 26257;
+
+ private static final String FIRST_VERSION_WITH_ENV_VARS_SUPPORT = "22.1.0";
+
+ private String databaseName = "postgres";
+
+ private String username = "root";
+
+ private String password = "";
+
+ private boolean isVersionGreaterThanOrEqualTo221;
+
+ public CockroachContainer(final String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ public CockroachContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ this.isVersionGreaterThanOrEqualTo221 = isVersionGreaterThanOrEqualTo221(dockerImageName);
+
+ WaitAllStrategy waitStrategy = new WaitAllStrategy();
+ waitStrategy.withStrategy(
+ Wait.forHttp("/health").forPort(REST_API_PORT).forStatusCode(200).withStartupTimeout(Duration.ofMinutes(1))
+ );
+ if (this.isVersionGreaterThanOrEqualTo221) {
+ waitStrategy.withStrategy(Wait.forSuccessfulCommand("[ -f ./init_success ] || { exit 1; }"));
+ }
+
+ withExposedPorts(REST_API_PORT, DB_PORT);
+ waitingFor(waitStrategy);
+ withCommand("start-single-node --insecure");
+ }
+
+ @Override
+ protected void configure() {
+ withEnv("COCKROACH_USER", this.username);
+ withEnv("COCKROACH_PASSWORD", this.password);
+ if (this.password != null && !this.password.isEmpty()) {
+ withCommand("start-single-node");
+ }
+ withEnv("COCKROACH_DATABASE", this.databaseName);
+ }
+
+ @Override
+ public String getDriverClassName() {
+ return JDBC_DRIVER_CLASS_NAME;
+ }
+
+ @Override
+ public String getJdbcUrl() {
+ String additionalUrlParams = constructUrlParameters("?", "&");
+ return (
+ JDBC_URL_PREFIX +
+ "://" +
+ getHost() +
+ ":" +
+ getMappedPort(DB_PORT) +
+ "/" +
+ databaseName +
+ additionalUrlParams
+ );
+ }
+
+ @Override
+ public final String getDatabaseName() {
+ return databaseName;
+ }
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String getTestQueryString() {
+ return TEST_QUERY_STRING;
+ }
+
+ @Override
+ public CockroachContainer withUsername(String username) {
+ validateIfVersionSupportsUsernameOrPasswordOrDatabase("username");
+ this.username = username;
+ return this;
+ }
+
+ @Override
+ public CockroachContainer withPassword(String password) {
+ validateIfVersionSupportsUsernameOrPasswordOrDatabase("password");
+ this.password = password;
+ return this;
+ }
+
+ @Override
+ public CockroachContainer withDatabaseName(final String databaseName) {
+ validateIfVersionSupportsUsernameOrPasswordOrDatabase("databaseName");
+ this.databaseName = databaseName;
+ return this;
+ }
+
+ private boolean isVersionGreaterThanOrEqualTo221(DockerImageName dockerImageName) {
+ ComparableVersion version = new ComparableVersion(dockerImageName.getVersionPart().replaceFirst("v", ""));
+ return version.isGreaterThanOrEqualTo(FIRST_VERSION_WITH_ENV_VARS_SUPPORT);
+ }
+
+ private void validateIfVersionSupportsUsernameOrPasswordOrDatabase(String parameter) {
+ if (!isVersionGreaterThanOrEqualTo221) {
+ throw new UnsupportedOperationException(
+ String.format("Setting a %s in not supported in the versions below 22.1.0", parameter)
+ );
+ }
+ }
+
+ @Override
+ protected void waitUntilContainerStarted() {
+ getWaitStrategy().waitUntilReady(this);
+ }
+}
diff --git a/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java b/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java
index dfec3988f15..461b0467767 100644
--- a/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java
+++ b/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java
@@ -1,10 +1,26 @@
package org.testcontainers.containers;
-import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
+import org.testcontainers.utility.ComparableVersion;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
+/**
+ * Testcontainers implementation for CockroachDB.
+ *
+ * Supported image: {@code cockroachdb/cockroach}
+ *
+ * Exposed ports:
+ *
+ * Database: 26257
+ * Console: 8080
+ *
+ *
+ * @deprecated use {@link org.testcontainers.cockroachdb.CockroachContainer} instead
+ */
+@Deprecated
public class CockroachContainer extends JdbcDatabaseContainer {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("cockroachdb/cockroach");
@@ -29,14 +45,18 @@ public class CockroachContainer extends JdbcDatabaseContainer System.out.println(outputFrame.getUtf8String()))
+ ) { // CockroachDB is expected to be compatible with Postgres
+ cockroach.start();
+
+ ResultSet resultSet = performQuery(cockroach, "SELECT foo FROM bar");
+
+ String firstColumnValue = resultSet.getString(1);
+ assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world");
+ }
+ }
+}
diff --git a/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java b/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java
index 86aaf6b4f63..097aeef005b 100644
--- a/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java
+++ b/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java
@@ -1,20 +1,16 @@
package org.testcontainers.jdbc.cockroachdb;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
import org.testcontainers.jdbc.AbstractJDBCDriverTest;
import java.util.Arrays;
import java.util.EnumSet;
-@RunWith(Parameterized.class)
-public class CockroachDBJDBCDriverTest extends AbstractJDBCDriverTest {
+class CockroachDBJDBCDriverTest extends AbstractJDBCDriverTest {
- @Parameterized.Parameters(name = "{index} - {0}")
public static Iterable data() {
return Arrays.asList(
new Object[][] { //
- { "jdbc:tc:cockroach://hostname/databasename", EnumSet.noneOf(Options.class) },
+ { "jdbc:tc:cockroach:v22.2.3://hostname/databasename", EnumSet.noneOf(Options.class) },
}
);
}
diff --git a/modules/cockroachdb/src/test/java/org/testcontainers/junit/cockroachdb/SimpleCockroachDBTest.java b/modules/cockroachdb/src/test/java/org/testcontainers/junit/cockroachdb/SimpleCockroachDBTest.java
deleted file mode 100644
index 65c79441f32..00000000000
--- a/modules/cockroachdb/src/test/java/org/testcontainers/junit/cockroachdb/SimpleCockroachDBTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package org.testcontainers.junit.cockroachdb;
-
-import org.junit.Test;
-import org.testcontainers.CockroachDBTestImages;
-import org.testcontainers.containers.CockroachContainer;
-import org.testcontainers.db.AbstractContainerDatabaseTest;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.logging.Level;
-import java.util.logging.LogManager;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class SimpleCockroachDBTest extends AbstractContainerDatabaseTest {
- static {
- // Postgres JDBC driver uses JUL; disable it to avoid annoying, irrelevant, stderr logs during connection testing
- LogManager.getLogManager().getLogger("").setLevel(Level.OFF);
- }
-
- @Test
- public void testSimple() throws SQLException {
- try (CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE)) {
- cockroach.start();
-
- ResultSet resultSet = performQuery(cockroach, "SELECT 1");
-
- int resultSetInt = resultSet.getInt(1);
- assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1);
- }
- }
-
- @Test
- public void testExplicitInitScript() throws SQLException {
- try (
- CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE)
- .withInitScript("somepath/init_postgresql.sql")
- ) { // CockroachDB is expected to be compatible with Postgres
- cockroach.start();
-
- ResultSet resultSet = performQuery(cockroach, "SELECT foo FROM bar");
-
- String firstColumnValue = resultSet.getString(1);
- assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world");
- }
- }
-
- @Test
- public void testWithAdditionalUrlParamInJdbcUrl() {
- CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE)
- .withUrlParam("sslmode", "disable")
- .withUrlParam("application_name", "cockroach");
-
- try {
- cockroach.start();
- String jdbcUrl = cockroach.getJdbcUrl();
- assertThat(jdbcUrl).contains("?");
- assertThat(jdbcUrl).contains("&");
- assertThat(jdbcUrl).contains("sslmode=disable");
- assertThat(jdbcUrl).contains("application_name=cockroach");
- } finally {
- cockroach.stop();
- }
- }
-}
diff --git a/modules/cockroachdb/src/test/resources/logback-test.xml b/modules/cockroachdb/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..83ef7a1a3ef
--- /dev/null
+++ b/modules/cockroachdb/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/modules/consul/build.gradle b/modules/consul/build.gradle
index a72aefaab5f..f496a86d6fd 100644
--- a/modules/consul/build.gradle
+++ b/modules/consul/build.gradle
@@ -4,6 +4,5 @@ dependencies {
api project(':testcontainers')
testImplementation 'com.ecwid.consul:consul-api:1.4.5'
- testImplementation 'io.rest-assured:rest-assured:5.2.0'
- testImplementation 'org.assertj:assertj-core:3.23.1'
+ testImplementation 'io.rest-assured:rest-assured:5.5.7'
}
diff --git a/modules/consul/src/main/java/org/testcontainers/consul/ConsulContainer.java b/modules/consul/src/main/java/org/testcontainers/consul/ConsulContainer.java
index 0c9d2607072..2922d9711ae 100644
--- a/modules/consul/src/main/java/org/testcontainers/consul/ConsulContainer.java
+++ b/modules/consul/src/main/java/org/testcontainers/consul/ConsulContainer.java
@@ -14,10 +14,20 @@
/**
* Testcontainers implementation for Consul.
+ *
+ * Supported images: {@code hashicorp/consul}, {@code consul}
+ *
+ * Exposed ports:
+ *
+ * HTTP: 8500
+ * gRPC: 8502
+ *
*/
public class ConsulContainer extends GenericContainer {
- private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("consul");
+ private static final DockerImageName DEFAULT_OLD_IMAGE_NAME = DockerImageName.parse("consul");
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("hashicorp/consul");
private static final int CONSUL_HTTP_PORT = 8500;
@@ -33,7 +43,7 @@ public ConsulContainer(String dockerImageName) {
public ConsulContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
- dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ dockerImageName.assertCompatibleWith(DEFAULT_OLD_IMAGE_NAME, DEFAULT_IMAGE_NAME);
// Use the status leader endpoint to verify if consul is running.
setWaitStrategy(Wait.forHttp("/v1/status/leader").forPort(CONSUL_HTTP_PORT).forStatusCode(200));
@@ -81,7 +91,7 @@ private void runConsulCommands() {
/**
* Run consul commands using the consul cli.
*
- * Useful for enableing more secret engines like:
+ * Useful for enabling more secret engines like:
*
* .withConsulCommand("secrets enable pki")
* .withConsulCommand("secrets enable transit")
diff --git a/modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java b/modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java
index c0c022d31bb..6e02bff51e8 100644
--- a/modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java
+++ b/modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java
@@ -4,8 +4,9 @@
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.kv.model.GetValue;
import io.restassured.RestAssured;
-import org.junit.ClassRule;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import java.io.IOException;
@@ -16,25 +17,30 @@
import static org.assertj.core.api.Assertions.assertThat;
-/**
- * This test shows the pattern to use the ConsulContainer @ClassRule for a junit test. It also has tests that ensure
- * the properties were added correctly by reading from Consul with the CLI and over HTTP.
- */
-public class ConsulContainerTest {
+class ConsulContainerTest {
- @ClassRule
- public static ConsulContainer consulContainer = new ConsulContainer(ConsulTestImages.CONSUL_IMAGE)
+ private static ConsulContainer consul = new ConsulContainer("hashicorp/consul:1.15")
.withConsulCommand("kv put config/testing1 value123");
+ @BeforeAll
+ static void setup() {
+ consul.start();
+ }
+
+ @AfterAll
+ static void teardown() {
+ consul.stop();
+ }
+
@Test
- public void readFirstPropertyPathWithCli() throws IOException, InterruptedException {
- GenericContainer.ExecResult result = consulContainer.execInContainer("consul", "kv", "get", "config/testing1");
+ void readFirstPropertyPathWithCli() throws IOException, InterruptedException {
+ GenericContainer.ExecResult result = consul.execInContainer("consul", "kv", "get", "config/testing1");
final String output = result.getStdout().replaceAll("\\r?\\n", "");
assertThat(output).contains("value123");
}
@Test
- public void readFirstSecretPathOverHttpApi() {
+ void readFirstSecretPathOverHttpApi() {
io.restassured.response.Response response = RestAssured
.given()
.when()
@@ -46,11 +52,8 @@ public void readFirstSecretPathOverHttpApi() {
}
@Test
- public void writeAndReadMultipleValuesUsingClient() {
- final ConsulClient consulClient = new ConsulClient(
- consulContainer.getHost(),
- consulContainer.getFirstMappedPort()
- );
+ void writeAndReadMultipleValuesUsingClient() {
+ final ConsulClient consulClient = new ConsulClient(consul.getHost(), consul.getFirstMappedPort());
final Map properties = new HashMap<>();
properties.put("value", "world");
@@ -70,6 +73,6 @@ public void writeAndReadMultipleValuesUsingClient() {
}
private String getHostAndPort() {
- return consulContainer.getHost() + ":" + consulContainer.getMappedPort(8500);
+ return consul.getHost() + ":" + consul.getMappedPort(8500);
}
}
diff --git a/modules/consul/src/test/java/org/testcontainers/consul/ConsulTestImages.java b/modules/consul/src/test/java/org/testcontainers/consul/ConsulTestImages.java
deleted file mode 100644
index de2b9702066..00000000000
--- a/modules/consul/src/test/java/org/testcontainers/consul/ConsulTestImages.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.testcontainers.consul;
-
-import org.testcontainers.utility.DockerImageName;
-
-public interface ConsulTestImages {
- DockerImageName CONSUL_IMAGE = DockerImageName.parse("consul:1.10.12");
-}
diff --git a/modules/consul/src/test/resources/logback-test.xml b/modules/consul/src/test/resources/logback-test.xml
index 535e406fc13..83ef7a1a3ef 100644
--- a/modules/consul/src/test/resources/logback-test.xml
+++ b/modules/consul/src/test/resources/logback-test.xml
@@ -12,5 +12,5 @@
-
+
diff --git a/modules/couchbase/build.gradle b/modules/couchbase/build.gradle
index c2aad1b4ffd..068e334e2f1 100644
--- a/modules/couchbase/build.gradle
+++ b/modules/couchbase/build.gradle
@@ -3,9 +3,8 @@ description = "Testcontainers :: Couchbase"
dependencies {
api project(':testcontainers')
// TODO use JDK's HTTP client and/or Apache HttpClient5
- shaded 'com.squareup.okhttp3:okhttp:4.10.0'
+ shaded 'com.squareup.okhttp3:okhttp:5.5.0'
- testImplementation 'com.couchbase.client:java-client:3.4.0'
- testImplementation 'org.awaitility:awaitility:4.2.0'
- testImplementation 'org.assertj:assertj-core:3.23.1'
+ testImplementation 'com.couchbase.client:java-client:3.11.2'
+ testImplementation 'org.awaitility:awaitility:4.3.0'
}
diff --git a/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java b/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java
index 2817b79d71f..14997d253af 100644
--- a/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java
+++ b/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java
@@ -48,7 +48,14 @@
import java.util.stream.Collectors;
/**
- * The couchbase container initializes and configures a Couchbase Server single node cluster.
+ * Testcontainers implementation for Couchbase.
+ *
+ * Supported image: {@code couchbase/server}
+ *
+ * Exposed ports:
+ *
*
* Note that it does not depend on a specific couchbase SDK, so it can be used with both the Java SDK 2 and 3 as well
* as the Scala SDK 1 or newer. We recommend using the latest and greatest SDKs for the best experience.
@@ -85,8 +92,6 @@ public class CouchbaseContainer extends GenericContainer {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("couchbase/server");
- private static final String DEFAULT_TAG = "6.5.1";
-
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final OkHttpClient HTTP_CLIENT = new OkHttpClient();
@@ -116,14 +121,7 @@ public class CouchbaseContainer extends GenericContainer {
private boolean isEnterprise = false;
- /**
- * Creates a new couchbase container with the default image and version.
- * @deprecated use {@link CouchbaseContainer(DockerImageName)} instead
- */
- @Deprecated
- public CouchbaseContainer() {
- this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG));
- }
+ private boolean hasTlsPorts = false;
/**
* Creates a new couchbase container with the specified image name.
@@ -332,12 +330,20 @@ private void exposePorts() {
}
}
+ @Override
+ protected void containerIsStarting(InspectContainerResponse containerInfo, boolean reused) {
+ if (!reused) {
+ containerIsStarting(containerInfo);
+ }
+ }
+
@Override
protected void containerIsStarting(final InspectContainerResponse containerInfo) {
logger().debug("Couchbase container is starting, performing configuration.");
timePhase("waitUntilNodeIsOnline", this::waitUntilNodeIsOnline);
timePhase("initializeIsEnterprise", this::initializeIsEnterprise);
+ timePhase("initializeHasTlsPorts", this::initializeHasTlsPorts);
timePhase("renameNode", this::renameNode);
timePhase("initializeServices", this::initializeServices);
timePhase("setMemoryQuotas", this::setMemoryQuotas);
@@ -349,6 +355,13 @@ protected void containerIsStarting(final InspectContainerResponse containerInfo)
}
}
+ @Override
+ protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) {
+ if (!reused) {
+ this.containerIsStarted(containerInfo);
+ }
+ }
+
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
timePhase("createBuckets", this::createBuckets);
@@ -387,6 +400,31 @@ private void initializeIsEnterprise() {
}
}
+ /**
+ * Initializes the {@link #hasTlsPorts} flag.
+ *
+ * Community Edition might support TLS one happy day, so use a "supports TLS" flag separate from
+ * the "enterprise edition" flag.
+ */
+ private void initializeHasTlsPorts() {
+ @Cleanup
+ Response response = doHttpRequest(MGMT_PORT, "/pools/default/nodeServices", "GET", null, true);
+
+ try {
+ String clusterTopology = response.body().string();
+ hasTlsPorts =
+ !MAPPER
+ .readTree(clusterTopology)
+ .path("nodesExt")
+ .path(0)
+ .path("services")
+ .path("mgmtSSL")
+ .isMissingNode();
+ } catch (IOException e) {
+ throw new IllegalStateException("Couchbase /pools/default/nodeServices did not return valid JSON");
+ }
+ }
+
/**
* Rebinds/renames the internal hostname.
*
@@ -496,33 +534,45 @@ private void configureExternalPorts() {
final FormBody.Builder builder = new FormBody.Builder();
builder.add("hostname", getHost());
builder.add("mgmt", Integer.toString(getMappedPort(MGMT_PORT)));
- builder.add("mgmtSSL", Integer.toString(getMappedPort(MGMT_SSL_PORT)));
+ if (hasTlsPorts) {
+ builder.add("mgmtSSL", Integer.toString(getMappedPort(MGMT_SSL_PORT)));
+ }
if (enabledServices.contains(CouchbaseService.KV)) {
builder.add("kv", Integer.toString(getMappedPort(KV_PORT)));
- builder.add("kvSSL", Integer.toString(getMappedPort(KV_SSL_PORT)));
builder.add("capi", Integer.toString(getMappedPort(VIEW_PORT)));
- builder.add("capiSSL", Integer.toString(getMappedPort(VIEW_SSL_PORT)));
+ if (hasTlsPorts) {
+ builder.add("kvSSL", Integer.toString(getMappedPort(KV_SSL_PORT)));
+ builder.add("capiSSL", Integer.toString(getMappedPort(VIEW_SSL_PORT)));
+ }
}
if (enabledServices.contains(CouchbaseService.QUERY)) {
builder.add("n1ql", Integer.toString(getMappedPort(QUERY_PORT)));
- builder.add("n1qlSSL", Integer.toString(getMappedPort(QUERY_SSL_PORT)));
+ if (hasTlsPorts) {
+ builder.add("n1qlSSL", Integer.toString(getMappedPort(QUERY_SSL_PORT)));
+ }
}
if (enabledServices.contains(CouchbaseService.SEARCH)) {
builder.add("fts", Integer.toString(getMappedPort(SEARCH_PORT)));
- builder.add("ftsSSL", Integer.toString(getMappedPort(SEARCH_SSL_PORT)));
+ if (hasTlsPorts) {
+ builder.add("ftsSSL", Integer.toString(getMappedPort(SEARCH_SSL_PORT)));
+ }
}
if (enabledServices.contains(CouchbaseService.ANALYTICS)) {
builder.add("cbas", Integer.toString(getMappedPort(ANALYTICS_PORT)));
- builder.add("cbasSSL", Integer.toString(getMappedPort(ANALYTICS_SSL_PORT)));
+ if (hasTlsPorts) {
+ builder.add("cbasSSL", Integer.toString(getMappedPort(ANALYTICS_SSL_PORT)));
+ }
}
if (enabledServices.contains(CouchbaseService.EVENTING)) {
builder.add("eventingAdminPort", Integer.toString(getMappedPort(EVENTING_PORT)));
- builder.add("eventingSSL", Integer.toString(getMappedPort(EVENTING_SSL_PORT)));
+ if (hasTlsPorts) {
+ builder.add("eventingSSL", Integer.toString(getMappedPort(EVENTING_SSL_PORT)));
+ }
}
@Cleanup
diff --git a/modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java b/modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java
index 691dc1307ef..b973d5eff2a 100644
--- a/modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java
+++ b/modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java
@@ -20,9 +20,8 @@
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.Collection;
import com.couchbase.client.java.json.JsonObject;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.testcontainers.containers.ContainerLaunchException;
-import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
import java.util.function.Consumer;
@@ -31,53 +30,45 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;
-public class CouchbaseContainerTest {
+class CouchbaseContainerTest {
- private static final DockerImageName COUCHBASE_IMAGE_ENTERPRISE = DockerImageName.parse(
- "couchbase/server:enterprise-7.0.3"
- );
+ private static final String COUCHBASE_IMAGE_ENTERPRISE = "couchbase/server:enterprise-7.0.3";
- private static final DockerImageName COUCHBASE_IMAGE_COMMUNITY = DockerImageName.parse(
- "couchbase/server:community-7.0.2"
- );
+ private static final String COUCHBASE_IMAGE_ENTERPRISE_RECENT = "couchbase/server:enterprise-7.6.2";
- @Test
- public void testBasicContainerUsageForEnterpriseContainer() {
- // bucket_definition {
- BucketDefinition bucketDefinition = new BucketDefinition("mybucket");
- // }
-
- try (
- // container_definition {
- CouchbaseContainer container = new CouchbaseContainer(COUCHBASE_IMAGE_ENTERPRISE)
- .withBucket(bucketDefinition)
- // }
- ) {
- setUpClient(
- container,
- cluster -> {
- Bucket bucket = cluster.bucket(bucketDefinition.getName());
- bucket.waitUntilReady(Duration.ofSeconds(10L));
+ private static final String COUCHBASE_IMAGE_COMMUNITY = "couchbase/server:community-7.0.2";
- Collection collection = bucket.defaultCollection();
+ private static final String COUCHBASE_IMAGE_COMMUNITY_RECENT = "couchbase/server:community-7.6.2";
- collection.upsert("foo", JsonObject.create().put("key", "value"));
+ @Test
+ void testBasicContainerUsageForEnterpriseContainer() {
+ testBasicContainerUsage(COUCHBASE_IMAGE_ENTERPRISE);
+ }
- JsonObject fooObject = collection.get("foo").contentAsObject();
+ @Test
+ void testBasicContainerUsageForEnterpriseContainerRecent() {
+ testBasicContainerUsage(COUCHBASE_IMAGE_ENTERPRISE_RECENT);
+ }
- assertThat(fooObject.getString("key")).isEqualTo("value");
- }
- );
- }
+ @Test
+ void testBasicContainerUsageForCommunityContainer() {
+ testBasicContainerUsage(COUCHBASE_IMAGE_COMMUNITY);
}
@Test
- public void testBasicContainerUsageForCommunityContainer() {
+ void testBasicContainerUsageForCommunityContainerRecent() {
+ testBasicContainerUsage(COUCHBASE_IMAGE_COMMUNITY_RECENT);
+ }
+
+ private void testBasicContainerUsage(String couchbaseImage) {
+ // bucket_definition {
BucketDefinition bucketDefinition = new BucketDefinition("mybucket");
+ // }
try (
- CouchbaseContainer container = new CouchbaseContainer(COUCHBASE_IMAGE_COMMUNITY)
- .withBucket(bucketDefinition)
+ // container_definition {
+ CouchbaseContainer container = new CouchbaseContainer(couchbaseImage).withBucket(bucketDefinition)
+ // }
) {
setUpClient(
container,
@@ -98,7 +89,7 @@ public void testBasicContainerUsageForCommunityContainer() {
}
@Test
- public void testBucketIsFlushableIfEnabled() {
+ void testBucketIsFlushableIfEnabled() {
BucketDefinition bucketDefinition = new BucketDefinition("mybucket").withFlushEnabled(true);
try (
@@ -128,7 +119,7 @@ public void testBucketIsFlushableIfEnabled() {
* edition which is not supported.
*/
@Test
- public void testFailureIfCommunityUsedWithAnalytics() {
+ void testFailureIfCommunityUsedWithAnalytics() {
try (
CouchbaseContainer container = new CouchbaseContainer(COUCHBASE_IMAGE_COMMUNITY)
.withEnabledServices(CouchbaseService.KV, CouchbaseService.ANALYTICS)
@@ -145,7 +136,7 @@ public void testFailureIfCommunityUsedWithAnalytics() {
* edition which is not supported.
*/
@Test
- public void testFailureIfCommunityUsedWithEventing() {
+ void testFailureIfCommunityUsedWithEventing() {
try (
CouchbaseContainer container = new CouchbaseContainer(COUCHBASE_IMAGE_COMMUNITY)
.withEnabledServices(CouchbaseService.KV, CouchbaseService.EVENTING)
diff --git a/modules/couchbase/src/test/resources/logback-test.xml b/modules/couchbase/src/test/resources/logback-test.xml
index 535e406fc13..83ef7a1a3ef 100644
--- a/modules/couchbase/src/test/resources/logback-test.xml
+++ b/modules/couchbase/src/test/resources/logback-test.xml
@@ -12,5 +12,5 @@
-
+
diff --git a/modules/cratedb/build.gradle b/modules/cratedb/build.gradle
new file mode 100644
index 00000000000..62405d559b2
--- /dev/null
+++ b/modules/cratedb/build.gradle
@@ -0,0 +1,11 @@
+description = "Testcontainers :: JDBC :: CrateDB"
+
+dependencies {
+ api project(':testcontainers-jdbc')
+
+ testRuntimeOnly 'org.postgresql:postgresql:42.7.12'
+
+ testImplementation project(':testcontainers-jdbc-test')
+
+ compileOnly 'org.jetbrains:annotations:26.1.0'
+}
diff --git a/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainer.java b/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainer.java
new file mode 100644
index 00000000000..d91704eba3d
--- /dev/null
+++ b/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainer.java
@@ -0,0 +1,128 @@
+package org.testcontainers.cratedb;
+
+import org.jetbrains.annotations.NotNull;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Set;
+
+/**
+ * Testcontainers implementation for CrateDB.
+ *
+ * Supported image: {@code crate}
+ *
+ * Exposed ports:
+ *
+ * Database: 5432
+ * Console: 4200
+ *
+ */
+public class CrateDBContainer extends JdbcDatabaseContainer {
+
+ static final String NAME = "cratedb";
+
+ static final String IMAGE = "crate";
+
+ static final String DEFAULT_TAG = "5.3.1";
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("crate");
+
+ static final Integer CRATEDB_PG_PORT = 5432;
+
+ static final Integer CRATEDB_HTTP_PORT = 4200;
+
+ private String databaseName = "crate";
+
+ private String username = "crate";
+
+ private String password = "crate";
+
+ public CrateDBContainer(final String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ public CrateDBContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ withCommand("crate -C discovery.type=single-node");
+
+ waitingFor(Wait.forHttp("/").forPort(CRATEDB_HTTP_PORT).forStatusCode(200));
+
+ addExposedPort(CRATEDB_PG_PORT);
+ addExposedPort(CRATEDB_HTTP_PORT);
+ }
+
+ /**
+ * @return the ports on which to check if the container is ready
+ * @deprecated use {@link #getLivenessCheckPortNumbers()} instead
+ */
+ @NotNull
+ @Override
+ @Deprecated
+ protected Set getLivenessCheckPorts() {
+ return super.getLivenessCheckPorts();
+ }
+
+ @Override
+ public String getDriverClassName() {
+ return "org.postgresql.Driver";
+ }
+
+ @Override
+ public String getJdbcUrl() {
+ String additionalUrlParams = constructUrlParameters("?", "&");
+ return (
+ "jdbc:postgresql://" +
+ getHost() +
+ ":" +
+ getMappedPort(CRATEDB_PG_PORT) +
+ "/" +
+ databaseName +
+ additionalUrlParams
+ );
+ }
+
+ @Override
+ public String getDatabaseName() {
+ return databaseName;
+ }
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String getTestQueryString() {
+ return "SELECT 1";
+ }
+
+ @Override
+ public CrateDBContainer withDatabaseName(final String databaseName) {
+ this.databaseName = databaseName;
+ return self();
+ }
+
+ @Override
+ public CrateDBContainer withUsername(final String username) {
+ this.username = username;
+ return self();
+ }
+
+ @Override
+ public CrateDBContainer withPassword(final String password) {
+ this.password = password;
+ return self();
+ }
+
+ @Override
+ protected void waitUntilContainerStarted() {
+ getWaitStrategy().waitUntilReady(this);
+ }
+}
diff --git a/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainerProvider.java b/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainerProvider.java
new file mode 100644
index 00000000000..0bf80f051ec
--- /dev/null
+++ b/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainerProvider.java
@@ -0,0 +1,36 @@
+package org.testcontainers.cratedb;
+
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.JdbcDatabaseContainerProvider;
+import org.testcontainers.jdbc.ConnectionUrl;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Factory for CrateDB containers using PostgreSQL JDBC driver.
+ */
+public class CrateDBContainerProvider extends JdbcDatabaseContainerProvider {
+
+ public static final String USER_PARAM = "user";
+
+ public static final String PASSWORD_PARAM = "password";
+
+ @Override
+ public boolean supports(String databaseType) {
+ return databaseType.equals(CrateDBContainer.NAME);
+ }
+
+ @Override
+ public JdbcDatabaseContainer newInstance() {
+ return newInstance(CrateDBContainer.DEFAULT_TAG);
+ }
+
+ @Override
+ public JdbcDatabaseContainer newInstance(String tag) {
+ return new CrateDBContainer(DockerImageName.parse(CrateDBContainer.IMAGE).withTag(tag));
+ }
+
+ @Override
+ public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) {
+ return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM);
+ }
+}
diff --git a/modules/cratedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/cratedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider
new file mode 100644
index 00000000000..ddd58ec8794
--- /dev/null
+++ b/modules/cratedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider
@@ -0,0 +1 @@
+org.testcontainers.cratedb.CrateDBContainerProvider
diff --git a/modules/cratedb/src/test/java/org/testcontainers/CrateDBTestImages.java b/modules/cratedb/src/test/java/org/testcontainers/CrateDBTestImages.java
new file mode 100644
index 00000000000..5025d409839
--- /dev/null
+++ b/modules/cratedb/src/test/java/org/testcontainers/CrateDBTestImages.java
@@ -0,0 +1,7 @@
+package org.testcontainers;
+
+import org.testcontainers.utility.DockerImageName;
+
+public interface CrateDBTestImages {
+ DockerImageName CRATEDB_TEST_IMAGE = DockerImageName.parse("crate:5.2.5");
+}
diff --git a/modules/cratedb/src/test/java/org/testcontainers/jdbc/cratedb/CrateDBJDBCDriverTest.java b/modules/cratedb/src/test/java/org/testcontainers/jdbc/cratedb/CrateDBJDBCDriverTest.java
new file mode 100644
index 00000000000..2c20afe6680
--- /dev/null
+++ b/modules/cratedb/src/test/java/org/testcontainers/jdbc/cratedb/CrateDBJDBCDriverTest.java
@@ -0,0 +1,17 @@
+package org.testcontainers.jdbc.cratedb;
+
+import org.testcontainers.jdbc.AbstractJDBCDriverTest;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+
+class CrateDBJDBCDriverTest extends AbstractJDBCDriverTest {
+
+ public static Iterable data() {
+ return Arrays.asList(
+ new Object[][] {
+ { "jdbc:tc:cratedb:5.2.3://hostname/crate?user=crate&password=somepwd", EnumSet.noneOf(Options.class) },
+ }
+ );
+ }
+}
diff --git a/modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java b/modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java
new file mode 100644
index 00000000000..803b9173218
--- /dev/null
+++ b/modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java
@@ -0,0 +1,70 @@
+package org.testcontainers.junit.cratedb;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.CrateDBTestImages;
+import org.testcontainers.cratedb.CrateDBContainer;
+import org.testcontainers.db.AbstractContainerDatabaseTest;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SimpleCrateDBTest extends AbstractContainerDatabaseTest {
+ static {
+ // Postgres JDBC driver uses JUL; disable it to avoid annoying, irrelevant, stderr logs during connection testing
+ LogManager.getLogManager().getLogger("").setLevel(Level.OFF);
+ }
+
+ @Test
+ void testSimple() throws SQLException {
+ try ( // container {
+ CrateDBContainer cratedb = new CrateDBContainer("crate:5.2.5")
+ // }
+ ) {
+ cratedb.start();
+
+ ResultSet resultSet = performQuery(cratedb, "SELECT 1");
+ int resultSetInt = resultSet.getInt(1);
+ assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1);
+ assertHasCorrectExposedAndLivenessCheckPorts(cratedb);
+ }
+ }
+
+ @Test
+ void testCommandOverride() throws SQLException {
+ try (
+ CrateDBContainer cratedb = new CrateDBContainer(CrateDBTestImages.CRATEDB_TEST_IMAGE)
+ .withCommand("crate -C discovery.type=single-node -C cluster.name=testcontainers")
+ ) {
+ cratedb.start();
+
+ ResultSet resultSet = performQuery(cratedb, "select name from sys.cluster");
+ String result = resultSet.getString(1);
+ assertThat(result).as("cluster name should be overridden").isEqualTo("testcontainers");
+ }
+ }
+
+ @Test
+ void testExplicitInitScript() throws SQLException {
+ try (
+ CrateDBContainer cratedb = new CrateDBContainer(CrateDBTestImages.CRATEDB_TEST_IMAGE)
+ .withInitScript("somepath/init_cratedb.sql")
+ ) {
+ cratedb.start();
+
+ ResultSet resultSet = performQuery(cratedb, "SELECT foo FROM bar");
+
+ String firstColumnValue = resultSet.getString(1);
+ assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world");
+ }
+ }
+
+ private void assertHasCorrectExposedAndLivenessCheckPorts(CrateDBContainer cratedb) {
+ assertThat(cratedb.getExposedPorts()).containsExactlyInAnyOrder(5432, 4200);
+ assertThat(cratedb.getLivenessCheckPortNumbers())
+ .containsExactlyInAnyOrder(cratedb.getMappedPort(5432), cratedb.getMappedPort(4200));
+ }
+}
diff --git a/modules/cratedb/src/test/resources/logback-test.xml b/modules/cratedb/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..83ef7a1a3ef
--- /dev/null
+++ b/modules/cratedb/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/modules/cratedb/src/test/resources/somepath/init_cratedb.sql b/modules/cratedb/src/test/resources/somepath/init_cratedb.sql
new file mode 100644
index 00000000000..4faa1ede65c
--- /dev/null
+++ b/modules/cratedb/src/test/resources/somepath/init_cratedb.sql
@@ -0,0 +1,6 @@
+CREATE TABLE bar (
+ foo STRING
+);
+
+INSERT INTO bar (foo) VALUES ('hello world');
+REFRESH TABLE bar;
diff --git a/modules/database-commons/build.gradle b/modules/database-commons/build.gradle
index 33a6d1e9826..345b354d739 100644
--- a/modules/database-commons/build.gradle
+++ b/modules/database-commons/build.gradle
@@ -2,6 +2,4 @@ description = "Testcontainers :: Database-Commons"
dependencies {
api project(':testcontainers')
-
- testImplementation 'org.assertj:assertj-core:3.23.1'
}
diff --git a/modules/database-commons/src/main/java/org/testcontainers/delegate/AbstractDatabaseDelegate.java b/modules/database-commons/src/main/java/org/testcontainers/delegate/AbstractDatabaseDelegate.java
index 196eb485b5f..a84535ed5fd 100644
--- a/modules/database-commons/src/main/java/org/testcontainers/delegate/AbstractDatabaseDelegate.java
+++ b/modules/database-commons/src/main/java/org/testcontainers/delegate/AbstractDatabaseDelegate.java
@@ -4,7 +4,6 @@
/**
* @param connection to the database
- * @author Eugeny Karpov
*/
public abstract class AbstractDatabaseDelegate implements DatabaseDelegate {
diff --git a/modules/database-commons/src/main/java/org/testcontainers/delegate/DatabaseDelegate.java b/modules/database-commons/src/main/java/org/testcontainers/delegate/DatabaseDelegate.java
index eda8f197008..e9dd207e112 100644
--- a/modules/database-commons/src/main/java/org/testcontainers/delegate/DatabaseDelegate.java
+++ b/modules/database-commons/src/main/java/org/testcontainers/delegate/DatabaseDelegate.java
@@ -6,8 +6,6 @@
* Database delegate
*
* Gives an abstraction from concrete database
- *
- * @author Eugeny Karpov
*/
public interface DatabaseDelegate extends AutoCloseable {
/**
diff --git a/modules/database-commons/src/main/java/org/testcontainers/exception/ConnectionCreationException.java b/modules/database-commons/src/main/java/org/testcontainers/exception/ConnectionCreationException.java
index 3b8852dbddc..0a117ac23d2 100644
--- a/modules/database-commons/src/main/java/org/testcontainers/exception/ConnectionCreationException.java
+++ b/modules/database-commons/src/main/java/org/testcontainers/exception/ConnectionCreationException.java
@@ -2,8 +2,6 @@
/**
* Inability to create connection to the database
- *
- * @author Eugeny Karpov
*/
public class ConnectionCreationException extends RuntimeException {
diff --git a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptScanner.java b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptScanner.java
new file mode 100644
index 00000000000..8350f7fbf38
--- /dev/null
+++ b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptScanner.java
@@ -0,0 +1,173 @@
+package org.testcontainers.ext;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Rough lexical parser for SQL scripts.
+ */
+@RequiredArgsConstructor
+class ScriptScanner {
+
+ private final String resource;
+
+ private final String script;
+
+ private final String separator;
+
+ private final String commentPrefix;
+
+ private final String blockCommentStartDelimiter;
+
+ private final String blockCommentEndDelimiter;
+
+ private final Pattern eol = Pattern.compile("[\n\r]+");
+
+ private final Pattern whitespace = Pattern.compile("\\s+");
+
+ private final Pattern identifier = Pattern.compile("[a-z][a-z0-9_$]*", Pattern.CASE_INSENSITIVE);
+
+ private final Pattern dollarQuotedStringDelimiter = Pattern.compile("\\$\\w*\\$");
+
+ private int offset;
+
+ @Getter
+ private String currentMatch;
+
+ private boolean matches(String substring) {
+ if (script.startsWith(substring, offset)) {
+ currentMatch = substring;
+ offset += currentMatch.length();
+ return true;
+ } else {
+ currentMatch = "";
+ return false;
+ }
+ }
+
+ private boolean matches(Pattern regexp) {
+ Matcher m = regexp.matcher(script);
+ m.region(offset, script.length());
+ if (m.lookingAt()) {
+ currentMatch = m.group();
+ offset = m.end();
+ return true;
+ } else {
+ currentMatch = "";
+ return false;
+ }
+ }
+
+ private boolean matchesSingleLineComment() {
+ /* Matches from commentPrefix to the EOL or end of script */
+ if (matches(commentPrefix)) {
+ Matcher m = eol.matcher(script);
+ if (m.find(offset)) {
+ currentMatch = commentPrefix + script.substring(offset, m.end());
+ offset = m.end();
+ } else {
+ currentMatch = commentPrefix + script.substring(offset);
+ offset = script.length();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean matchesMultilineComment() {
+ /* Matches from blockCommentStartDelimiter to the next blockCommentEndDelimiter.
+ * Error, if blockCommentEndDelimiter is not found. */
+ if (matches(blockCommentStartDelimiter)) {
+ int end = script.indexOf(blockCommentEndDelimiter, offset);
+ if (end < 0) {
+ throw new ScriptUtils.ScriptParseException(
+ String.format("Missing block comment end delimiter [%s].", blockCommentEndDelimiter),
+ resource
+ );
+ }
+ end += blockCommentEndDelimiter.length();
+ currentMatch = blockCommentStartDelimiter + script.substring(offset, end);
+ offset = end;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean matchesQuotedString(final char quote) {
+ if (script.charAt(offset) == quote) {
+ boolean escaped = false;
+ for (int i = offset + 1; i < script.length(); i++) {
+ char c = script.charAt(i);
+ if (escaped) {
+ //just skip the escaped character and drop the flag
+ escaped = false;
+ } else if (c == '\\') {
+ escaped = true;
+ } else if (c == quote) {
+ currentMatch = script.substring(offset, i + 1);
+ offset = i + 1;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean matchesDollarQuotedString() {
+ //Matches $$ .... $$
+ if (matches(dollarQuotedStringDelimiter)) {
+ String delimiter = currentMatch;
+ int end = script.indexOf(delimiter, offset);
+ if (end < 0) {
+ throw new ScriptUtils.ScriptParseException(
+ String.format("Unclosed dollar quoted string [%s].", delimiter),
+ resource
+ );
+ }
+ end += delimiter.length();
+ currentMatch = delimiter + script.substring(offset, end);
+ offset = end;
+ return true;
+ }
+ return false;
+ }
+
+ Lexem next() {
+ if (offset < script.length()) {
+ if (matches(separator)) {
+ return Lexem.SEPARATOR;
+ } else if (matchesSingleLineComment() || matchesMultilineComment()) {
+ return Lexem.COMMENT;
+ } else if (
+ matchesQuotedString('\'') ||
+ matchesQuotedString('"') ||
+ matchesQuotedString('`') ||
+ matchesDollarQuotedString()
+ ) {
+ return Lexem.QUOTED_STRING;
+ } else if (matches(identifier)) {
+ return Lexem.IDENTIFIER;
+ } else if (matches(whitespace)) {
+ return Lexem.WHITESPACE;
+ } else {
+ currentMatch = String.valueOf(script.charAt(offset++));
+ return Lexem.OTHER;
+ }
+ } else {
+ return Lexem.EOF;
+ }
+ }
+
+ enum Lexem {
+ SEPARATOR,
+ COMMENT,
+ QUOTED_STRING,
+ WHITESPACE,
+ IDENTIFIER,
+ OTHER,
+ EOF,
+ }
+}
diff --git a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptSplitter.java b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptSplitter.java
new file mode 100644
index 00000000000..d71f6f3ac3d
--- /dev/null
+++ b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptSplitter.java
@@ -0,0 +1,127 @@
+package org.testcontainers.ext;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.testcontainers.ext.ScriptScanner.Lexem;
+
+import java.util.List;
+
+/**
+ * Performs splitting of an SQL script into statements including
+ * basic clean-up.
+ */
+@RequiredArgsConstructor
+class ScriptSplitter {
+
+ private final ScriptScanner scanner;
+
+ private final List statements;
+
+ private final StringBuilder sb = new StringBuilder();
+
+ /**
+ * Standard parsing:
+ * 1. Remove comments
+ * 2. Shrink whitespace and eols
+ * 3. Split on separator
+ */
+ void split() {
+ Lexem l;
+ while ((l = scanner.next()) != Lexem.EOF) {
+ switch (l) {
+ case SEPARATOR:
+ flushStringBuilder();
+ break;
+ case COMMENT:
+ //skip
+ break;
+ case WHITESPACE:
+ if (sb.length() == 0 || sb.charAt(sb.length() - 1) != ' ') {
+ sb.append(' ');
+ }
+ break;
+ case IDENTIFIER:
+ appendMatch();
+ if ("begin".equalsIgnoreCase(scanner.getCurrentMatch())) {
+ compoundStatement(false);
+ flushStringBuilder();
+ }
+ break;
+ default:
+ appendMatch();
+ }
+ }
+ flushStringBuilder();
+ }
+
+ /**
+ * Compound statement ('create procedure') mode:
+ * 1. Do not remove comments
+ * 2. Do not shrink whitespace
+ * 3. Do not split on separators
+ * 3. This mode can be recursive
+ */
+ private void compoundStatement(boolean recursive) {
+ Lexem l;
+ while ((l = scanner.next()) != Lexem.EOF) {
+ appendMatch();
+ if (Lexem.IDENTIFIER.equals(l)) {
+ if ("begin".equalsIgnoreCase(scanner.getCurrentMatch())) {
+ compoundStatement(true);
+ } else if ("end".equalsIgnoreCase(scanner.getCurrentMatch())) {
+ if (endOfBlock(recursive)) {
+ return;
+ }
+ }
+ }
+ }
+ flushStringBuilder();
+ }
+
+ private boolean endOfBlock(boolean recursive) {
+ Lexem l;
+ StringBuilder temporary = new StringBuilder();
+ while ((l = scanner.next()) != Lexem.EOF) {
+ switch (l) {
+ case COMMENT:
+ case WHITESPACE:
+ temporary.append(scanner.getCurrentMatch());
+ break;
+ case SEPARATOR:
+ //Only whitespace and comments preceded the separator: true end of block
+ //If it's an internal block, append everything
+ if (recursive) {
+ sb.append(temporary);
+ appendMatch();
+ }
+ return true;
+ default:
+ // Semicolon is not recognized as separator: this means that a custom
+ // separator is used. Still, 'END;' should be a valid end of block
+ if (";".equals(scanner.getCurrentMatch())) {
+ if (recursive) {
+ sb.append(temporary);
+ }
+ appendMatch();
+ return true;
+ }
+ sb.append(temporary);
+ appendMatch();
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void appendMatch() {
+ sb.append(scanner.getCurrentMatch());
+ }
+
+ private void flushStringBuilder() {
+ final String s = sb.toString().trim();
+ if (StringUtils.isNotEmpty(s)) {
+ statements.add(s);
+ }
+ sb.setLength(0);
+ }
+}
diff --git a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java
index eca4989c70e..b78ac4d63c4 100644
--- a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java
+++ b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java
@@ -27,6 +27,7 @@
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import javax.script.ScriptException;
@@ -36,16 +37,6 @@
*
* Generic utility methods for working with SQL scripts. Mainly for internal use
* within the framework.
- *
- * @author Thomas Risberg
- * @author Sam Brannen
- * @author Juergen Hoeller
- * @author Keith Donald
- * @author Dave Syer
- * @author Chris Beams
- * @author Oliver Gierke
- * @author Chris Baldwin
- * @since 4.0.3
*/
public abstract class ScriptUtils {
@@ -129,191 +120,18 @@ public static void splitSqlScript(
"blockCommentEndDelimiter must not be null or empty"
);
- StringBuilder sb = new StringBuilder();
- boolean inEscape = false;
- boolean inLineComment = false;
- boolean inBlockComment = false;
- Character currentLiteralDelimiter = null;
-
- int compoundStatementDepth = 0;
- final String lowerCaseScriptContent = script.toLowerCase();
- char[] content = script.toCharArray();
- for (int i = 0; i < script.length(); i++) {
- char c = content[i];
- if (inEscape) {
- inEscape = false;
- sb.append(c);
- continue;
- }
- // MySQL style escapes
- if (c == '\\') {
- inEscape = true;
- sb.append(c);
- continue;
- }
- // Determine whether we're entering/leaving a string literal
- if (!inBlockComment && !inLineComment && (c == '\'' || c == '"' || c == '`')) {
- if (currentLiteralDelimiter == null) { // ignore delimiters within an existing string literal
- currentLiteralDelimiter = c;
- } else if (currentLiteralDelimiter == c) { // find end of string literal
- currentLiteralDelimiter = null;
- }
- }
- final boolean inLiteral = currentLiteralDelimiter != null;
-
- if (!inLiteral && containsSubstringAtOffset(lowerCaseScriptContent, commentPrefix, i)) {
- inLineComment = true;
- }
- if (inLineComment && c == '\n') {
- inLineComment = false;
- }
- if (!inLiteral && containsSubstringAtOffset(lowerCaseScriptContent, blockCommentStartDelimiter, i)) {
- inBlockComment = true;
- }
- if (
- !inLiteral &&
- inBlockComment &&
- containsSubstringAtOffset(lowerCaseScriptContent, blockCommentEndDelimiter, i)
- ) {
- inBlockComment = false;
- }
- final boolean inComment = inLineComment || inBlockComment;
-
- if (
- !inLiteral &&
- !inComment &&
- containsKeywordsAtOffset(
- lowerCaseScriptContent,
- "BEGIN",
- i,
- separator,
- commentPrefix,
- blockCommentStartDelimiter
- )
- ) {
- compoundStatementDepth++;
- }
- if (
- !inLiteral &&
- !inComment &&
- containsKeywordsAtOffset(
- lowerCaseScriptContent,
- "END",
- i,
- separator,
- commentPrefix,
- blockCommentStartDelimiter
- )
- ) {
- compoundStatementDepth--;
- }
- final boolean inCompoundStatement = compoundStatementDepth != 0;
-
- if (!inLiteral && !inCompoundStatement) {
- if (script.startsWith(separator, i)) {
- // we've reached the end of the current statement
- sb = flushStringBuilder(sb, statements);
- i += separator.length() - 1;
- continue;
- } else if (script.startsWith(commentPrefix, i)) {
- // skip over any content from the start of the comment to the EOL
- int indexOfNextNewline = script.indexOf("\n", i);
- if (indexOfNextNewline > i) {
- i = indexOfNextNewline;
- continue;
- } else {
- // if there's no EOL, we must be at the end
- // of the script, so stop here.
- break;
- }
- } else if (script.startsWith(blockCommentStartDelimiter, i)) {
- // skip over any block comments
- int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i);
- if (indexOfCommentEnd > i) {
- i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1;
- inBlockComment = false;
- continue;
- } else {
- throw new ScriptParseException(
- String.format("Missing block comment end delimiter [%s].", blockCommentEndDelimiter),
- resource
- );
- }
- } else if (c == ' ' || c == '\n' || c == '\t' || c == '\r') {
- // avoid multiple adjacent whitespace characters
- if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
- c = ' ';
- } else {
- continue;
- }
- }
- }
- sb.append(c);
- }
- flushStringBuilder(sb, statements);
- }
-
- private static StringBuilder flushStringBuilder(StringBuilder sb, List statements) {
- if (sb.length() == 0) {
- return sb;
- }
-
- final String s = sb.toString().trim();
- if (StringUtils.isNotEmpty(s)) {
- statements.add(s);
- }
-
- return new StringBuilder();
- }
-
- private static boolean isSeperator(
- char c,
- String separator,
- String commentPrefix,
- String blockCommentStartDelimiter
- ) {
- return (
- c == ' ' ||
- c == '\r' ||
- c == '\n' ||
- c == '\t' ||
- c == separator.charAt(0) ||
- c == separator.charAt(separator.length() - 1) ||
- c == commentPrefix.charAt(0) ||
- c == blockCommentStartDelimiter.charAt(0) ||
- c == blockCommentStartDelimiter.charAt(blockCommentStartDelimiter.length() - 1)
- );
- }
-
- private static boolean containsSubstringAtOffset(String lowercaseString, String substring, int offset) {
- String lowercaseSubstring = substring.toLowerCase();
-
- return lowercaseString.startsWith(lowercaseSubstring, offset);
- }
-
- private static boolean containsKeywordsAtOffset(
- String lowercaseString,
- String keywords,
- int offset,
- String separator,
- String commentPrefix,
- String blockCommentStartDelimiter
- ) {
- String lowercaseKeywords = keywords.toLowerCase();
-
- boolean backSeperated =
- (offset == 0) ||
- isSeperator(lowercaseString.charAt(offset - 1), separator, commentPrefix, blockCommentStartDelimiter);
- boolean frontSeperated =
- (offset >= (lowercaseString.length() - keywords.length())) ||
- isSeperator(
- lowercaseString.charAt(offset + keywords.length()),
+ new ScriptSplitter(
+ new ScriptScanner(
+ resource,
+ script,
separator,
commentPrefix,
- blockCommentStartDelimiter
- );
-
- return backSeperated && frontSeperated && lowercaseString.startsWith(lowercaseKeywords, offset);
+ blockCommentStartDelimiter,
+ blockCommentEndDelimiter
+ ),
+ statements
+ )
+ .split();
}
private static void checkArgument(boolean expression, String errorMessage) {
@@ -328,13 +146,45 @@ private static void checkArgument(boolean expression, String errorMessage) {
* @param delim String delimiting each statement - typically a ';' character
*/
public static boolean containsSqlScriptDelimiters(String script, String delim) {
- boolean inLiteral = false;
- char[] content = script.toCharArray();
- for (int i = 0; i < script.length(); i++) {
- if (content[i] == '\'') {
- inLiteral = !inLiteral;
- }
- if (!inLiteral && script.startsWith(delim, i)) {
+ return containsSqlScriptDelimiters(
+ "",
+ script,
+ DEFAULT_COMMENT_PREFIX,
+ delim,
+ DEFAULT_BLOCK_COMMENT_START_DELIMITER,
+ DEFAULT_BLOCK_COMMENT_END_DELIMITER
+ );
+ }
+
+ /**
+ * Does the provided SQL script contain the specified delimiter?
+ *
+ * @param script the SQL script
+ * @param delim String delimiting each statement - typically a ';' character
+ * @param commentPrefix the prefix that identifies comments in the SQL script,
+ * typically "--"
+ * @param blockCommentStartDelimiter block comment start delimiter
+ * @param blockCommentEndDelimiter block comment end delimiter
+ */
+ public static boolean containsSqlScriptDelimiters(
+ String scriptPath,
+ String script,
+ String commentPrefix,
+ String delim,
+ String blockCommentStartDelimiter,
+ String blockCommentEndDelimiter
+ ) {
+ ScriptScanner scanner = new ScriptScanner(
+ scriptPath,
+ script,
+ delim,
+ commentPrefix,
+ blockCommentStartDelimiter,
+ blockCommentEndDelimiter
+ );
+ ScriptScanner.Lexem l;
+ while ((l = scanner.next()) != ScriptScanner.Lexem.EOF) {
+ if (ScriptScanner.Lexem.SEPARATOR.equals(l)) {
return true;
}
}
@@ -424,13 +274,22 @@ public static void executeDatabaseScript(
LOGGER.info("Executing database script from " + scriptPath);
}
- long startTime = System.currentTimeMillis();
+ long startTime = System.nanoTime();
List statements = new LinkedList<>();
if (separator == null) {
separator = DEFAULT_STATEMENT_SEPARATOR;
}
- if (!containsSqlScriptDelimiters(script, separator)) {
+ if (
+ !containsSqlScriptDelimiters(
+ scriptPath,
+ script,
+ commentPrefix,
+ separator,
+ blockCommentStartDelimiter,
+ blockCommentEndDelimiter
+ )
+ ) {
separator = FALLBACK_STATEMENT_SEPARATOR;
}
@@ -448,7 +307,7 @@ public static void executeDatabaseScript(
closeableDelegate.execute(statements, scriptPath, continueOnError, ignoreFailedDrops);
}
- long elapsedTime = System.currentTimeMillis() - startTime;
+ long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Executed database script from " + scriptPath + " in " + elapsedTime + " ms.");
}
diff --git a/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptScannerTest.java b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptScannerTest.java
new file mode 100644
index 00000000000..a82305a0a6e
--- /dev/null
+++ b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptScannerTest.java
@@ -0,0 +1,54 @@
+package org.testcontainers.ext;
+
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.Test;
+
+import java.util.regex.Pattern;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ScriptScannerTest {
+
+ @Test
+ void testHugeStringLiteral() {
+ String script = "/* a comment */ \"" + StringUtils.repeat('~', 10000) + "\";";
+ ScriptScanner scanner = scanner(script);
+ assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.COMMENT);
+ assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.WHITESPACE);
+ assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.QUOTED_STRING);
+ assertThat(scanner.getCurrentMatch()).matches(Pattern.compile("\"~+\""));
+ }
+
+ @Test
+ void testPgIdentifierWithDollarSigns() {
+ ScriptScanner scanner = scanner(
+ "this$is$a$valid$postgreSQL$identifier " +
+ "$a$While this is a quoted string$a$$ --just followed by a dollar sign"
+ );
+ assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.IDENTIFIER);
+ assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.WHITESPACE);
+ assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.QUOTED_STRING);
+ assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.OTHER);
+ }
+
+ @Test
+ void testQuotedLiterals() {
+ ScriptScanner scanner = scanner("'this \\'is a literal' \"this \\\" is a literal\"");
+ assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.QUOTED_STRING);
+ assertThat(scanner.getCurrentMatch()).isEqualTo("'this \\'is a literal'");
+ assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.WHITESPACE);
+ assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.QUOTED_STRING);
+ assertThat(scanner.getCurrentMatch()).isEqualTo("\"this \\\" is a literal\"");
+ }
+
+ private static ScriptScanner scanner(String script) {
+ return new ScriptScanner(
+ "dummy",
+ script,
+ ScriptUtils.DEFAULT_STATEMENT_SEPARATOR,
+ ScriptUtils.DEFAULT_COMMENT_PREFIX,
+ ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER,
+ ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER
+ );
+ }
+}
diff --git a/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java
index 99ebb69a1e8..cb0e33162f4 100644
--- a/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java
+++ b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java
@@ -1,18 +1,19 @@
package org.testcontainers.ext;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.fail;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
-public class ScriptSplittingTest {
+class ScriptSplittingTest {
@Test
- public void testStringDemarcation() {
+ void testStringDemarcation() {
String script = "SELECT 'foo `bar`'; SELECT 'foo -- `bar`'; SELECT 'foo /* `bar`';";
List expected = Arrays.asList("SELECT 'foo `bar`'", "SELECT 'foo -- `bar`'", "SELECT 'foo /* `bar`'");
@@ -21,7 +22,7 @@ public void testStringDemarcation() {
}
@Test
- public void testIssue1547Case1() {
+ void testIssue1547Case1() {
String script =
"create database if not exists ttt;\n" +
"\n" +
@@ -50,7 +51,7 @@ public void testIssue1547Case1() {
}
@Test
- public void testIssue1547Case2() {
+ void testIssue1547Case2() {
String script =
"CREATE TABLE bar (\n" +
" end_time VARCHAR(255)\n" +
@@ -68,7 +69,16 @@ public void testIssue1547Case2() {
}
@Test
- public void testUnusualSemicolonPlacement() {
+ void testSplittingEnquotedSemicolon() {
+ String script = "CREATE TABLE `bar;bar` (\n" + " end_time VARCHAR(255)\n" + ");";
+
+ List expected = Arrays.asList("CREATE TABLE `bar;bar` ( end_time VARCHAR(255) )");
+
+ splitAndCompare(script, expected);
+ }
+
+ @Test
+ void testUnusualSemicolonPlacement() {
String script = "SELECT 1;;;;;SELECT 2;\n;SELECT 3\n; SELECT 4;\n SELECT 5";
List expected = Arrays.asList("SELECT 1", "SELECT 2", "SELECT 3", "SELECT 4", "SELECT 5");
@@ -77,7 +87,7 @@ public void testUnusualSemicolonPlacement() {
}
@Test
- public void testCommentedSemicolon() {
+ void testCommentedSemicolon() {
String script =
"CREATE TABLE bar (\n" + " foo VARCHAR(255)\n" + "); \nDROP PROCEDURE IF EXISTS -- ;\n" + " count_foo";
@@ -90,7 +100,7 @@ public void testCommentedSemicolon() {
}
@Test
- public void testStringEscaping() {
+ void testStringEscaping() {
String script =
"SELECT \"a /* string literal containing comment characters like -- here\";\n" +
"SELECT \"a 'quoting' \\\"scenario ` involving BEGIN keyword\\\" here\";\n" +
@@ -106,7 +116,7 @@ public void testStringEscaping() {
}
@Test
- public void testBlockCommentExclusion() {
+ void testBlockCommentExclusion() {
String script = "INSERT INTO bar (foo) /* ; */ VALUES ('hello world');";
List expected = Arrays.asList("INSERT INTO bar (foo) VALUES ('hello world')");
@@ -115,7 +125,7 @@ public void testBlockCommentExclusion() {
}
@Test
- public void testBeginEndKeywordCorrectDetection() {
+ void testBeginEndKeywordCorrectDetection() {
String script =
"INSERT INTO something_end (begin_with_the_token, another_field) /*end*/ VALUES /* end */ (' begin ', `end`)-- begin\n;";
@@ -127,7 +137,7 @@ public void testBeginEndKeywordCorrectDetection() {
}
@Test
- public void testCommentInStrings() {
+ void testCommentInStrings() {
String script =
"CREATE TABLE bar (foo VARCHAR(255));\n" +
"\n" +
@@ -151,7 +161,7 @@ public void testCommentInStrings() {
}
@Test
- public void testMultipleBeginEndDetection() {
+ void testMultipleBeginEndDetection() {
String script =
"CREATE TABLE bar (foo VARCHAR(255));\n" +
"\n" +
@@ -195,7 +205,7 @@ public void testMultipleBeginEndDetection() {
}
@Test
- public void testProcedureBlock() {
+ void testProcedureBlock() {
String script =
"CREATE PROCEDURE count_foo()\n" +
" BEGIN\n" +
@@ -254,19 +264,15 @@ public void testProcedureBlock() {
}
@Test
- public void testUnclosedBlockComment() {
+ void testUnclosedBlockComment() {
String script = "SELECT 'foo `bar`'; /*";
-
- try {
- doSplit(script);
- fail("Should have thrown!");
- } catch (ScriptUtils.ScriptParseException expected) {
- // ignore expected exception
- }
+ assertThatThrownBy(() -> doSplit(script, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR))
+ .isInstanceOf(ScriptUtils.ScriptParseException.class)
+ .hasMessageContaining("*/");
}
@Test
- public void testIssue1452Case() {
+ void testIssue1452Case() {
String script =
"create table test (text VARCHAR(255));\n" +
"\n" +
@@ -281,17 +287,180 @@ public void testIssue1452Case() {
splitAndCompare(script, expected);
}
+ @Test
+ void testIfLoopBlocks() {
+ String script =
+ "BEGIN\n" +
+ " rec_loop: LOOP\n" +
+ " FETCH blah;\n" +
+ " IF something_wrong THEN LEAVE rec_loop; END IF;\n" +
+ " do_something_else;\n" +
+ " END LOOP;\n" +
+ "END /* final comment */;";
+ List expected = Collections.singletonList(
+ "BEGIN\n" +
+ " rec_loop: LOOP\n" +
+ " FETCH blah;\n" +
+ " IF something_wrong THEN LEAVE rec_loop; END IF;\n" +
+ " do_something_else;\n" +
+ " END LOOP;\n" +
+ "END"
+ );
+ splitAndCompare(script, expected);
+ }
+
+ @Test
+ void testIfLoopBlocksSpecificSeparator() {
+ String script =
+ "BEGIN\n" +
+ " rec_loop: LOOP\n" +
+ " FETCH blah;\n" +
+ " IF something_wrong THEN LEAVE rec_loop; END IF;\n" +
+ " do_something_else;\n" +
+ " END LOOP;\n" +
+ "END;\n" +
+ "@\n" +
+ "CALL something();\n" +
+ "@\n";
+ List expected = Arrays.asList(
+ "BEGIN\n" +
+ " rec_loop: LOOP\n" +
+ " FETCH blah;\n" +
+ " IF something_wrong THEN LEAVE rec_loop; END IF;\n" +
+ " do_something_else;\n" +
+ " END LOOP;\n" +
+ "END;",
+ "CALL something();"
+ );
+ splitAndCompare(script, expected, "@");
+ }
+
+ @Test
+ void oracleStyleBlocks() {
+ String script = "BEGIN END; /\n" + "BEGIN END;";
+ List expected = Arrays.asList("BEGIN END;", "BEGIN END;");
+ splitAndCompare(script, expected, "/");
+ }
+
+ @Test
+ void testMultiProcedureMySQLScript() {
+ String script =
+ "CREATE PROCEDURE doiterate(p1 INT)\n" +
+ " BEGIN\n" +
+ " label1: LOOP\n" +
+ " SET p1 = p1 + 1;\n" +
+ " IF p1 < 10 THEN\n" +
+ " ITERATE label1;\n" +
+ " END IF;\n" +
+ " LEAVE label1;\n" +
+ " END LOOP label1;\n" +
+ " END;\n" +
+ "\n" +
+ "CREATE PROCEDURE dowhile()\n" +
+ " BEGIN\n" +
+ " DECLARE v1 INT DEFAULT 5;\n" +
+ " WHILE v1 > 0 DO\n" +
+ " SET v1 = v1 - 1;\n" +
+ " END WHILE;\n" +
+ " END;\n" +
+ "\n" +
+ "CREATE PROCEDURE dorepeat(p1 INT)\n" +
+ " BEGIN\n" +
+ " SET @x = 0;\n" +
+ " REPEAT\n" +
+ " SET @x = @x + 1;\n" +
+ " UNTIL @x > p1 END REPEAT;\n" +
+ " END;";
+ List expected = Arrays.asList(
+ "CREATE PROCEDURE doiterate(p1 INT) BEGIN\n" +
+ " label1: LOOP\n" +
+ " SET p1 = p1 + 1;\n" +
+ " IF p1 < 10 THEN\n" +
+ " ITERATE label1;\n" +
+ " END IF;\n" +
+ " LEAVE label1;\n" +
+ " END LOOP label1;\n" +
+ " END",
+ "CREATE PROCEDURE dowhile() BEGIN\n" +
+ " DECLARE v1 INT DEFAULT 5;\n" +
+ " WHILE v1 > 0 DO\n" +
+ " SET v1 = v1 - 1;\n" +
+ " END WHILE;\n" +
+ " END",
+ "CREATE PROCEDURE dorepeat(p1 INT) BEGIN\n" +
+ " SET @x = 0;\n" +
+ " REPEAT\n" +
+ " SET @x = @x + 1;\n" +
+ " UNTIL @x > p1 END REPEAT;\n" +
+ " END"
+ );
+ splitAndCompare(script, expected);
+ }
+
+ @Test
+ void testDollarQuotedStrings() {
+ String script =
+ "CREATE FUNCTION f ()\n" +
+ "RETURNS INT\n" +
+ "AS $$\n" +
+ "BEGIN\n" +
+ " RETURN 1;\n" +
+ "END;\n" +
+ "$$ LANGUAGE plpgsql;";
+ List expected = Collections.singletonList(
+ "CREATE FUNCTION f () RETURNS INT AS $$\n" +
+ "BEGIN\n" +
+ " RETURN 1;\n" +
+ "END;\n" +
+ "$$ LANGUAGE plpgsql"
+ );
+ splitAndCompare(script, expected);
+ }
+
+ @Test
+ void testNestedDollarQuotedString() {
+ //see https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-DOLLAR-QUOTING
+ String script =
+ "CREATE FUNCTION f() AS $function$\n" +
+ "BEGIN\n" +
+ " RETURN ($1 ~ $q$[\\t\\r\\n\\v\\\\]$q$);\n" +
+ "END;\n" +
+ "$function$;" +
+ "create table foo ();";
+ List expected = Arrays.asList(
+ "CREATE FUNCTION f() AS $function$\n" +
+ "BEGIN\n" +
+ " RETURN ($1 ~ $q$[\\t\\r\\n\\v\\\\]$q$);\n" +
+ "END;\n" +
+ "$function$",
+ "create table foo ()"
+ );
+ splitAndCompare(script, expected);
+ }
+
+ @Test
+ void testUnclosedDollarQuotedString() {
+ String script = "SELECT $tag$ ..... $";
+ assertThatThrownBy(() -> doSplit(script, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR))
+ .isInstanceOf(ScriptUtils.ScriptParseException.class)
+ .hasMessageContaining("$tag$");
+ }
+
private void splitAndCompare(String script, List expected) {
- final List statements = doSplit(script);
+ splitAndCompare(script, expected, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR);
+ }
+
+ private void splitAndCompare(String script, List expected, String separator) {
+ final List statements = doSplit(script, separator);
assertThat(statements).isEqualTo(expected);
}
- private List doSplit(String script) {
+ private List doSplit(String script, String separator) {
final List statements = new ArrayList<>();
ScriptUtils.splitSqlScript(
"ignored",
script,
- ScriptUtils.DEFAULT_STATEMENT_SEPARATOR,
+ separator,
ScriptUtils.DEFAULT_COMMENT_PREFIX,
ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER,
ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER,
@@ -299,4 +468,14 @@ private List doSplit(String script) {
);
return statements;
}
+
+ @Test
+ void testIgnoreDelimitersInLiteralsAndComments() {
+ assertThat(ScriptUtils.containsSqlScriptDelimiters("'@' /*@*/ \"@\" $tag$@$tag$ --@", "@")).isFalse();
+ }
+
+ @Test
+ void testContainsDelimiters() {
+ assertThat(ScriptUtils.containsSqlScriptDelimiters("'@' /*@*/ @ \"@\" --@", "@")).isTrue();
+ }
}
diff --git a/modules/databend/build.gradle b/modules/databend/build.gradle
new file mode 100644
index 00000000000..7a779d3ca20
--- /dev/null
+++ b/modules/databend/build.gradle
@@ -0,0 +1,8 @@
+description = "Testcontainers :: JDBC :: Databend"
+
+dependencies {
+ api project(':testcontainers-jdbc')
+
+ testImplementation project(':testcontainers-jdbc-test')
+ testRuntimeOnly 'com.databend:databend-jdbc:0.4.6'
+}
diff --git a/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainer.java b/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainer.java
new file mode 100644
index 00000000000..389c8c44963
--- /dev/null
+++ b/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainer.java
@@ -0,0 +1,112 @@
+package org.testcontainers.databend;
+
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Testcontainers implementation for Databend.
+ *
+ * Supported image: {@code datafuselabs/databend}
+ *
+ * Exposed ports:
+ *
+ */
+public class DatabendContainer extends JdbcDatabaseContainer {
+
+ static final String NAME = "databend";
+
+ static final DockerImageName DOCKER_IMAGE_NAME = DockerImageName.parse("datafuselabs/databend");
+
+ private static final Integer HTTP_PORT = 8000;
+
+ private static final String DRIVER_CLASS_NAME = "com.databend.jdbc.DatabendDriver";
+
+ private static final String JDBC_URL_PREFIX = "jdbc:" + NAME + "://";
+
+ private static final String TEST_QUERY = "SELECT 1";
+
+ private String databaseName = "default";
+
+ private String username = "databend";
+
+ private String password = "databend";
+
+ public DatabendContainer(String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ public DatabendContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DOCKER_IMAGE_NAME);
+
+ addExposedPorts(HTTP_PORT);
+ waitingFor(Wait.forHttp("/").forResponsePredicate(response -> response.equals("Ok.")));
+ }
+
+ @Override
+ protected void configure() {
+ withEnv("QUERY_DEFAULT_USER", this.username);
+ withEnv("QUERY_DEFAULT_PASSWORD", this.password);
+ }
+
+ @Override
+ public Set getLivenessCheckPortNumbers() {
+ return new HashSet<>(getMappedPort(HTTP_PORT));
+ }
+
+ @Override
+ public String getDriverClassName() {
+ return DRIVER_CLASS_NAME;
+ }
+
+ @Override
+ public String getJdbcUrl() {
+ return (
+ JDBC_URL_PREFIX +
+ getHost() +
+ ":" +
+ getMappedPort(HTTP_PORT) +
+ "/" +
+ this.databaseName +
+ constructUrlParameters("?", "&")
+ );
+ }
+
+ @Override
+ public String getUsername() {
+ return this.username;
+ }
+
+ @Override
+ public String getPassword() {
+ return this.password;
+ }
+
+ @Override
+ public String getDatabaseName() {
+ return this.databaseName;
+ }
+
+ @Override
+ public String getTestQueryString() {
+ return TEST_QUERY;
+ }
+
+ @Override
+ public DatabendContainer withUsername(String username) {
+ this.username = username;
+ return this;
+ }
+
+ @Override
+ public DatabendContainer withPassword(String password) {
+ this.password = password;
+ return this;
+ }
+}
diff --git a/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainerProvider.java b/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainerProvider.java
new file mode 100644
index 00000000000..9142bebcfd3
--- /dev/null
+++ b/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainerProvider.java
@@ -0,0 +1,28 @@
+package org.testcontainers.databend;
+
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.JdbcDatabaseContainerProvider;
+
+public class DatabendContainerProvider extends JdbcDatabaseContainerProvider {
+
+ private static final String DEFAULT_TAG = "v1.2.615";
+
+ @Override
+ public boolean supports(String databaseType) {
+ return databaseType.equals(DatabendContainer.NAME);
+ }
+
+ @Override
+ public JdbcDatabaseContainer newInstance() {
+ return newInstance(DEFAULT_TAG);
+ }
+
+ @Override
+ public JdbcDatabaseContainer newInstance(String tag) {
+ if (tag != null) {
+ return new DatabendContainer(DatabendContainer.DOCKER_IMAGE_NAME.withTag(tag));
+ } else {
+ return newInstance();
+ }
+ }
+}
diff --git a/modules/databend/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/databend/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider
new file mode 100644
index 00000000000..ead69e77bee
--- /dev/null
+++ b/modules/databend/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider
@@ -0,0 +1 @@
+org.testcontainers.databend.DatabendContainerProvider
diff --git a/modules/databend/src/test/java/org/testcontainers/databend/DatabendContainerTest.java b/modules/databend/src/test/java/org/testcontainers/databend/DatabendContainerTest.java
new file mode 100644
index 00000000000..984f42979c4
--- /dev/null
+++ b/modules/databend/src/test/java/org/testcontainers/databend/DatabendContainerTest.java
@@ -0,0 +1,44 @@
+package org.testcontainers.databend;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.db.AbstractContainerDatabaseTest;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DatabendContainerTest extends AbstractContainerDatabaseTest {
+
+ @Test
+ void testSimple() throws SQLException {
+ try ( // container {
+ DatabendContainer databend = new DatabendContainer("datafuselabs/databend:v1.2.615")
+ // }
+ ) {
+ databend.start();
+
+ ResultSet resultSet = performQuery(databend, "SELECT 1");
+
+ int resultSetInt = resultSet.getInt(1);
+ assertThat(resultSetInt).isEqualTo(1);
+ }
+ }
+
+ @Test
+ void customCredentialsWithUrlParams() throws SQLException {
+ try (
+ DatabendContainer databend = new DatabendContainer("datafuselabs/databend:v1.2.615")
+ .withUsername("test")
+ .withPassword("test")
+ .withUrlParam("ssl", "false")
+ ) {
+ databend.start();
+
+ ResultSet resultSet = performQuery(databend, "SELECT 1;");
+
+ int resultSetInt = resultSet.getInt(1);
+ assertThat(resultSetInt).isEqualTo(1);
+ }
+ }
+}
diff --git a/modules/databend/src/test/java/org/testcontainers/databend/DatabendJDBCDriverTest.java b/modules/databend/src/test/java/org/testcontainers/databend/DatabendJDBCDriverTest.java
new file mode 100644
index 00000000000..2f3a030919f
--- /dev/null
+++ b/modules/databend/src/test/java/org/testcontainers/databend/DatabendJDBCDriverTest.java
@@ -0,0 +1,17 @@
+package org.testcontainers.databend;
+
+import org.testcontainers.jdbc.AbstractJDBCDriverTest;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+
+class DatabendJDBCDriverTest extends AbstractJDBCDriverTest {
+
+ public static Iterable data() {
+ return Arrays.asList(
+ new Object[][] { //
+ { "jdbc:tc:databend://hostname/databasename", EnumSet.of(Options.PmdKnownBroken) },
+ }
+ );
+ }
+}
diff --git a/modules/databend/src/test/resources/logback-test.xml b/modules/databend/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..83ef7a1a3ef
--- /dev/null
+++ b/modules/databend/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/modules/db2/build.gradle b/modules/db2/build.gradle
index 5021e958c60..afae0bfd751 100644
--- a/modules/db2/build.gradle
+++ b/modules/db2/build.gradle
@@ -1,9 +1,8 @@
description = "Testcontainers :: JDBC :: DB2"
dependencies {
- api project(':jdbc')
+ api project(':testcontainers-jdbc')
- testImplementation project(':jdbc-test')
- testImplementation 'com.ibm.db2:jcc:11.5.8.0'
- testImplementation 'org.assertj:assertj-core:3.23.1'
+ testImplementation project(':testcontainers-jdbc-test')
+ testRuntimeOnly 'com.ibm.db2:jcc:12.1.5.0'
}
diff --git a/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java b/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java
index 333a2e709c3..19dbc6a58ac 100644
--- a/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java
+++ b/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java
@@ -1,5 +1,6 @@
package org.testcontainers.containers;
+import com.github.dockerjava.api.model.Capability;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.LicenseAcceptance;
@@ -8,12 +9,27 @@
import java.time.temporal.ChronoUnit;
import java.util.Set;
+/**
+ * Testcontainers implementation for IBM DB2.
+ *
+ * Supported images: {@code icr.io/db2_community/db2}, {@code ibmcom/db2}
+ *
+ * Exposed ports:
+ *
+ * @deprecated use {@link org.testcontainers.db2.Db2Container} instead.
+ */
+@Deprecated
public class Db2Container extends JdbcDatabaseContainer {
public static final String NAME = "db2";
+ @Deprecated
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("ibmcom/db2");
+ private static final DockerImageName DEFAULT_NEW_IMAGE_NAME = DockerImageName.parse("icr.io/db2_community/db2");
+
@Deprecated
public static final String DEFAULT_DB2_IMAGE_NAME = DEFAULT_IMAGE_NAME.getUnversionedPart();
@@ -29,7 +45,7 @@ public class Db2Container extends JdbcDatabaseContainer {
private String password = "foobar1234";
/**
- * @deprecated use {@link Db2Container(DockerImageName)} instead
+ * @deprecated use {@link #Db2Container(DockerImageName)} instead
*/
@Deprecated
public Db2Container() {
@@ -42,9 +58,9 @@ public Db2Container(String dockerImageName) {
public Db2Container(final DockerImageName dockerImageName) {
super(dockerImageName);
- dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ dockerImageName.assertCompatibleWith(DEFAULT_NEW_IMAGE_NAME, DEFAULT_IMAGE_NAME);
- withPrivilegedMode(true);
+ withCreateContainerCmdModifier(cmd -> cmd.withCapAdd(Capability.IPC_LOCK).withCapAdd(Capability.IPC_OWNER));
this.waitStrategy =
new LogMessageWaitStrategy()
.withRegEx(".*Setup has completed\\..*")
@@ -65,7 +81,7 @@ protected Set getLivenessCheckPorts() {
@Override
protected void configure() {
- // If license was not accepted programatically, check if it was accepted via resource file
+ // If license was not accepted programmatically, check if it was accepted via resource file
if (!getEnvMap().containsKey("LICENSE")) {
LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName());
acceptLicense();
diff --git a/modules/db2/src/main/java/org/testcontainers/db2/Db2Container.java b/modules/db2/src/main/java/org/testcontainers/db2/Db2Container.java
new file mode 100644
index 00000000000..bdd182599e5
--- /dev/null
+++ b/modules/db2/src/main/java/org/testcontainers/db2/Db2Container.java
@@ -0,0 +1,143 @@
+package org.testcontainers.db2;
+
+import com.github.dockerjava.api.model.Capability;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.LicenseAcceptance;
+
+import java.time.Duration;
+import java.util.Set;
+
+/**
+ * Testcontainers implementation for IBM DB2.
+ *
+ * Supported images: {@code icr.io/db2_community/db2}, {@code ibmcom/db2}
+ *
+ * Exposed ports:
+ *
+ */
+public class Db2Container extends JdbcDatabaseContainer {
+
+ public static final String NAME = "db2";
+
+ private static final DockerImageName DEFAULT_NEW_IMAGE_NAME = DockerImageName.parse("icr.io/db2_community/db2");
+
+ public static final int DB2_PORT = 50000;
+
+ private String databaseName = "test";
+
+ private String username = "db2inst1";
+
+ private String password = "foobar1234";
+
+ public Db2Container(String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ public Db2Container(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_NEW_IMAGE_NAME);
+
+ withCreateContainerCmdModifier(cmd -> cmd.withCapAdd(Capability.IPC_LOCK).withCapAdd(Capability.IPC_OWNER));
+ waitingFor(Wait.forLogMessage(".*Setup has completed\\..*", 1).withStartupTimeout(Duration.ofMinutes(10)));
+
+ addExposedPort(DB2_PORT);
+ }
+
+ /**
+ * @return the ports on which to check if the container is ready
+ * @deprecated use {@link #getLivenessCheckPortNumbers()} instead
+ */
+ @Override
+ @Deprecated
+ protected Set getLivenessCheckPorts() {
+ return super.getLivenessCheckPorts();
+ }
+
+ @Override
+ protected void configure() {
+ // If license was not accepted programmatically, check if it was accepted via resource file
+ if (!getEnvMap().containsKey("LICENSE")) {
+ LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName());
+ acceptLicense();
+ }
+
+ addEnv("DBNAME", databaseName);
+ addEnv("DB2INSTANCE", username);
+ addEnv("DB2INST1_PASSWORD", password);
+
+ // These settings help the DB2 container start faster
+ if (!getEnvMap().containsKey("AUTOCONFIG")) {
+ addEnv("AUTOCONFIG", "false");
+ }
+ if (!getEnvMap().containsKey("ARCHIVE_LOGS")) {
+ addEnv("ARCHIVE_LOGS", "false");
+ }
+ }
+
+ /**
+ * Accepts the license for the DB2 container by setting the LICENSE=accept
+ * variable as described at https://hub.docker.com/r/ibmcom/db2
+ */
+ public Db2Container acceptLicense() {
+ addEnv("LICENSE", "accept");
+ return this;
+ }
+
+ @Override
+ public String getDriverClassName() {
+ return "com.ibm.db2.jcc.DB2Driver";
+ }
+
+ @Override
+ public String getJdbcUrl() {
+ String additionalUrlParams = constructUrlParameters(":", ";", ";");
+ return "jdbc:db2://" + getHost() + ":" + getMappedPort(DB2_PORT) + "/" + databaseName + additionalUrlParams;
+ }
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String getDatabaseName() {
+ return databaseName;
+ }
+
+ @Override
+ public Db2Container withUsername(String username) {
+ this.username = username;
+ return this;
+ }
+
+ @Override
+ public Db2Container withPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+ @Override
+ public Db2Container withDatabaseName(String dbName) {
+ this.databaseName = dbName;
+ return this;
+ }
+
+ @Override
+ protected void waitUntilContainerStarted() {
+ getWaitStrategy().waitUntilReady(this);
+ }
+
+ @Override
+ protected String getTestQueryString() {
+ return "SELECT 1 FROM SYSIBM.SYSDUMMY1";
+ }
+}
diff --git a/modules/db2/src/test/java/org/testcontainers/Db2TestImages.java b/modules/db2/src/test/java/org/testcontainers/Db2TestImages.java
index b0bfc869272..002d030b1c5 100644
--- a/modules/db2/src/test/java/org/testcontainers/Db2TestImages.java
+++ b/modules/db2/src/test/java/org/testcontainers/Db2TestImages.java
@@ -3,5 +3,5 @@
import org.testcontainers.utility.DockerImageName;
public interface Db2TestImages {
- DockerImageName DB2_IMAGE = DockerImageName.parse("ibmcom/db2:11.5.0.0a");
+ DockerImageName DB2_IMAGE = DockerImageName.parse("icr.io/db2_community/db2:11.5.8.0");
}
diff --git a/modules/db2/src/test/java/org/testcontainers/junit/db2/SimpleDb2Test.java b/modules/db2/src/test/java/org/testcontainers/db2/Db2ContainerTest.java
similarity index 57%
rename from modules/db2/src/test/java/org/testcontainers/junit/db2/SimpleDb2Test.java
rename to modules/db2/src/test/java/org/testcontainers/db2/Db2ContainerTest.java
index ae35a8ee77c..6b17d1583f7 100644
--- a/modules/db2/src/test/java/org/testcontainers/junit/db2/SimpleDb2Test.java
+++ b/modules/db2/src/test/java/org/testcontainers/db2/Db2ContainerTest.java
@@ -1,8 +1,7 @@
-package org.testcontainers.junit.db2;
+package org.testcontainers.db2;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.testcontainers.Db2TestImages;
-import org.testcontainers.containers.Db2Container;
import org.testcontainers.db.AbstractContainerDatabaseTest;
import java.sql.ResultSet;
@@ -10,11 +9,27 @@
import static org.assertj.core.api.Assertions.assertThat;
-public class SimpleDb2Test extends AbstractContainerDatabaseTest {
+class Db2ContainerTest extends AbstractContainerDatabaseTest {
@Test
- public void testSimple() throws SQLException {
- try (Db2Container db2 = new Db2Container(Db2TestImages.DB2_IMAGE).acceptLicense()) {
+ void testSimple() throws SQLException {
+ try ( // container {
+ Db2Container db2 = new Db2Container("icr.io/db2_community/db2:11.5.8.0").acceptLicense()
+ // }
+ ) {
+ db2.start();
+
+ ResultSet resultSet = performQuery(db2, "SELECT 1 FROM SYSIBM.SYSDUMMY1");
+
+ int resultSetInt = resultSet.getInt(1);
+ assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1);
+ assertHasCorrectExposedAndLivenessCheckPorts(db2);
+ }
+ }
+
+ @Test
+ void testSimpleWithNewImage() throws SQLException {
+ try (Db2Container db2 = new Db2Container("icr.io/db2_community/db2:11.5.8.0").acceptLicense()) {
db2.start();
ResultSet resultSet = performQuery(db2, "SELECT 1 FROM SYSIBM.SYSDUMMY1");
@@ -26,7 +41,7 @@ public void testSimple() throws SQLException {
}
@Test
- public void testWithAdditionalUrlParamInJdbcUrl() {
+ void testWithAdditionalUrlParamInJdbcUrl() {
try (
Db2Container db2 = new Db2Container(Db2TestImages.DB2_IMAGE)
.withUrlParam("sslConnection", "false")
diff --git a/modules/db2/src/test/java/org/testcontainers/jdbc/db2/DB2JDBCDriverTest.java b/modules/db2/src/test/java/org/testcontainers/jdbc/db2/DB2JDBCDriverTest.java
index 4a03f824a28..cfa74c8017a 100644
--- a/modules/db2/src/test/java/org/testcontainers/jdbc/db2/DB2JDBCDriverTest.java
+++ b/modules/db2/src/test/java/org/testcontainers/jdbc/db2/DB2JDBCDriverTest.java
@@ -1,16 +1,12 @@
package org.testcontainers.jdbc.db2;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
import org.testcontainers.jdbc.AbstractJDBCDriverTest;
import java.util.Arrays;
import java.util.EnumSet;
-@RunWith(Parameterized.class)
-public class DB2JDBCDriverTest extends AbstractJDBCDriverTest {
+class DB2JDBCDriverTest extends AbstractJDBCDriverTest {
- @Parameterized.Parameters(name = "{index} - {0}")
public static Iterable data() {
return Arrays.asList(
new Object[][] { //
diff --git a/modules/db2/src/test/resources/logback-test.xml b/modules/db2/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..83ef7a1a3ef
--- /dev/null
+++ b/modules/db2/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/modules/dynalite/build.gradle b/modules/dynalite/build.gradle
deleted file mode 100644
index ee77d7e00bb..00000000000
--- a/modules/dynalite/build.gradle
+++ /dev/null
@@ -1,9 +0,0 @@
-description = "Testcontainers :: Dynalite"
-
-dependencies {
- api project(':testcontainers')
-
- compileOnly 'com.amazonaws:aws-java-sdk-dynamodb:1.12.333'
- testImplementation 'com.amazonaws:aws-java-sdk-dynamodb:1.12.314'
- testImplementation 'org.assertj:assertj-core:3.23.1'
-}
diff --git a/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java b/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java
deleted file mode 100644
index 401fa8782b1..00000000000
--- a/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package org.testcontainers.dynamodb;
-
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.auth.AWSStaticCredentialsProvider;
-import com.amazonaws.auth.BasicAWSCredentials;
-import com.amazonaws.client.builder.AwsClientBuilder;
-import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
-import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
-import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.utility.DockerImageName;
-
-/**
- * Container for Dynalite, a DynamoDB clone.
- */
-public class DynaliteContainer extends GenericContainer {
-
- private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("quay.io/testcontainers/dynalite");
-
- private static final String DEFAULT_TAG = "v1.2.1-1";
-
- private static final int MAPPED_PORT = 4567;
-
- /**
- * @deprecated use {@link DynaliteContainer(DockerImageName)} instead
- */
- @Deprecated
- public DynaliteContainer() {
- this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG));
- }
-
- public DynaliteContainer(String dockerImageName) {
- this(DockerImageName.parse(dockerImageName));
- }
-
- public DynaliteContainer(final DockerImageName dockerImageName) {
- super(dockerImageName);
- dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
-
- withExposedPorts(MAPPED_PORT);
- }
-
- /**
- * Gets a preconfigured {@link AmazonDynamoDB} client object for connecting to this
- * container.
- *
- * @return preconfigured client
- */
- public AmazonDynamoDB getClient() {
- return AmazonDynamoDBClientBuilder
- .standard()
- .withEndpointConfiguration(getEndpointConfiguration())
- .withCredentials(getCredentials())
- .build();
- }
-
- /**
- * Gets {@link AwsClientBuilder.EndpointConfiguration}
- * that may be used to connect to this container.
- *
- * @return endpoint configuration
- */
- public AwsClientBuilder.EndpointConfiguration getEndpointConfiguration() {
- return new AwsClientBuilder.EndpointConfiguration(
- "http://" + this.getHost() + ":" + this.getMappedPort(MAPPED_PORT),
- null
- );
- }
-
- /**
- * Gets an {@link AWSCredentialsProvider} that may be used to connect to this container.
- *
- * @return dummy AWS credentials
- */
- public AWSCredentialsProvider getCredentials() {
- return new AWSStaticCredentialsProvider(new BasicAWSCredentials("dummy", "dummy"));
- }
-}
diff --git a/modules/dynalite/src/test/java/org/testcontainers/dynamodb/DynaliteContainerTest.java b/modules/dynalite/src/test/java/org/testcontainers/dynamodb/DynaliteContainerTest.java
deleted file mode 100644
index 10e02566d16..00000000000
--- a/modules/dynalite/src/test/java/org/testcontainers/dynamodb/DynaliteContainerTest.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.testcontainers.dynamodb;
-
-import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
-import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
-import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
-import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
-import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
-import com.amazonaws.services.dynamodbv2.model.KeyType;
-import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
-import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
-import com.amazonaws.services.dynamodbv2.model.TableDescription;
-import org.junit.Rule;
-import org.junit.Test;
-import org.testcontainers.utility.DockerImageName;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class DynaliteContainerTest {
-
- private static final DockerImageName DYNALITE_IMAGE = DockerImageName.parse(
- "quay.io/testcontainers/dynalite:v1.2.1-1"
- );
-
- @Rule
- public DynaliteContainer dynamoDB = new DynaliteContainer(DYNALITE_IMAGE);
-
- @Test
- public void simpleTestWithManualClientCreation() {
- final AmazonDynamoDB client = AmazonDynamoDBClientBuilder
- .standard()
- .withEndpointConfiguration(dynamoDB.getEndpointConfiguration())
- .withCredentials(dynamoDB.getCredentials())
- .build();
-
- runTest(client);
- }
-
- @Test
- public void simpleTestWithProvidedClient() {
- final AmazonDynamoDB client = dynamoDB.getClient();
-
- runTest(client);
- }
-
- private void runTest(AmazonDynamoDB client) {
- CreateTableRequest request = new CreateTableRequest()
- .withAttributeDefinitions(new AttributeDefinition("Name", ScalarAttributeType.S))
- .withKeySchema(new KeySchemaElement("Name", KeyType.HASH))
- .withProvisionedThroughput(new ProvisionedThroughput(10L, 10L))
- .withTableName("foo");
-
- client.createTable(request);
-
- final TableDescription tableDescription = client.describeTable("foo").getTable();
-
- assertThat(tableDescription).as("the description is not null").isNotNull();
- assertThat(tableDescription.getTableName()).as("the table has the right name").isEqualTo("foo");
- assertThat(tableDescription.getKeySchema().get(0).getAttributeName())
- .as("the name has the right primary key")
- .isEqualTo("Name");
- }
-}
diff --git a/modules/elasticsearch/build.gradle b/modules/elasticsearch/build.gradle
index a8001ca61e4..7f57916e991 100644
--- a/modules/elasticsearch/build.gradle
+++ b/modules/elasticsearch/build.gradle
@@ -1,8 +1,8 @@
-description = "TestContainers :: elasticsearch"
+description = "Testcontainers :: elasticsearch"
dependencies {
api project(':testcontainers')
- testImplementation "org.elasticsearch.client:elasticsearch-rest-client:8.5.0"
- testImplementation "org.elasticsearch.client:transport:7.17.7"
- testImplementation 'org.assertj:assertj-core:3.23.1'
+
+ testImplementation "org.elasticsearch.client:elasticsearch-rest-client:9.4.3"
+ testImplementation "org.elasticsearch.client:transport:7.17.29"
}
diff --git a/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java
index edef04e001e..e08db06581e 100644
--- a/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java
+++ b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java
@@ -1,14 +1,14 @@
package org.testcontainers.elasticsearch;
-import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.exception.NotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
-import org.testcontainers.utility.Base58;
+import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
+import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.ComparableVersion;
import org.testcontainers.utility.DockerImageName;
@@ -17,14 +17,23 @@
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
+import java.time.Duration;
+import java.time.Instant;
import java.util.Optional;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
/**
- * Represents an elasticsearch docker instance which exposes by default port 9200 and 9300 (transport.tcp.port)
- * The docker image is by default fetched from docker.elastic.co/elasticsearch/elasticsearch
+ * Testcontainers implementation for Elasticsearch.
+ *
+ * Supported image: {@code docker.elastic.co/elasticsearch/elasticsearch}, {@code elasticsearch}
+ *
+ * Exposed ports:
+ *
+ * HTTP: 9200
+ * TCP Transport: 9300
+ *
*/
@Slf4j
public class ElasticsearchContainer extends GenericContainer {
@@ -53,34 +62,28 @@ public class ElasticsearchContainer extends GenericContainer= 8
+ private static final String DEFAULT_CERT_PATH = "/usr/share/elasticsearch/config/certs/http_ca.crt";
- private final boolean isAtLeastMajorVersion8;
+ @Deprecated
+ private boolean isOss = false;
- private Optional caCertAsBytes = Optional.empty();
+ private final boolean isAtLeastMajorVersion8;
- private String certPath = "/usr/share/elasticsearch/config/certs/http_ca.crt";
+ private String certPath = "";
- /**
- * @deprecated use {@link ElasticsearchContainer(DockerImageName)} instead
- */
- @Deprecated
- public ElasticsearchContainer() {
- this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG));
- }
+ private Duration healthCheckTimeout = Duration.ofSeconds(60);
/**
* Create an Elasticsearch Container by passing the full docker image name
+ *
* @param dockerImageName Full docker image name as a {@link String}, like: docker.elastic.co/elasticsearch/elasticsearch:7.9.2
*/
public ElasticsearchContainer(String dockerImageName) {
@@ -89,16 +92,25 @@ public ElasticsearchContainer(String dockerImageName) {
/**
* Create an Elasticsearch Container by passing the full docker image name
+ *
* @param dockerImageName Full docker image name as a {@link DockerImageName}, like: DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:7.9.2")
*/
public ElasticsearchContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
- dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, DEFAULT_OSS_IMAGE_NAME);
- this.isOss = dockerImageName.isCompatibleWith(DEFAULT_OSS_IMAGE_NAME);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, DEFAULT_OSS_IMAGE_NAME, ELASTICSEARCH_IMAGE_NAME);
+
+ if (dockerImageName.isCompatibleWith(DEFAULT_OSS_IMAGE_NAME)) {
+ this.isOss = true;
+ log.warn(
+ "{} is not supported anymore after 7.10.2. Please switch to {}",
+ dockerImageName.getUnversionedPart(),
+ DEFAULT_IMAGE_NAME.getUnversionedPart()
+ );
+ }
- logger().info("Starting an elasticsearch container using [{}]", dockerImageName);
- withNetworkAliases("elasticsearch-" + Base58.randomString(6));
withEnv("discovery.type", "single-node");
+ // disable disk threshold checks
+ withEnv("cluster.routing.allocation.disk.threshold_enabled", "false");
// Sets default memory of elasticsearch instance to 2GB
// Spaces are deliberate to allow user to define additional jvm options as elasticsearch resolves option files lexicographically
withClasspathResourceMapping(
@@ -107,34 +119,14 @@ public ElasticsearchContainer(final DockerImageName dockerImageName) {
BindMode.READ_ONLY
);
addExposedPorts(ELASTICSEARCH_DEFAULT_PORT, ELASTICSEARCH_DEFAULT_TCP_PORT);
- this.isAtLeastMajorVersion8 =
- new ComparableVersion(dockerImageName.getVersionPart()).isGreaterThanOrEqualTo("8.0.0");
- // regex that
- // matches 8.3 JSON logging with started message and some follow up content within the message field
- // matches 8.0 JSON logging with no whitespace between message field and content
- // matches 7.x JSON logging with whitespace between message field and content
- // matches 6.x text logging with node name in brackets and just a 'started' message till the end of the line
- String regex = ".*(\"message\":\\s?\"started[\\s?|\"].*|] started\n$)";
- setWaitStrategy(new LogMessageWaitStrategy().withRegEx(regex));
+ String versionPart = dockerImageName.getVersionPart();
+ this.isAtLeastMajorVersion8 = new ComparableVersion(versionPart).isGreaterThanOrEqualTo("8.0.0");
+ // Wait strategy is deferred to configure() so it can read the final env map
+ // (e.g. password and SSL settings that the user may set after construction).
+ setWaitStrategy(null);
if (isAtLeastMajorVersion8) {
withPassword(ELASTICSEARCH_DEFAULT_PASSWORD);
- }
- }
-
- @Override
- protected void containerIsStarted(InspectContainerResponse containerInfo) {
- if (isAtLeastMajorVersion8 && StringUtils.isNotEmpty(certPath)) {
- try {
- byte[] bytes = copyFileFromContainer(certPath, IOUtils::toByteArray);
- if (bytes.length > 0) {
- this.caCertAsBytes = Optional.of(bytes);
- }
- } catch (NotFoundException e) {
- // just emit an error message, but do not throw an exception
- // this might be ok, if the docker image is accidentally looking like version 8 or latest
- // can happen if Elasticsearch is repackaged, i.e. with custom plugins
- log.warn("CA cert under " + certPath + " not found.");
- }
+ withCertPath(DEFAULT_CERT_PATH);
}
}
@@ -144,17 +136,36 @@ protected void containerIsStarted(InspectContainerResponse containerInfo) {
* @return byte array optional containing the CA cert extracted from the docker container
*/
public Optional caCertAsBytes() {
- return caCertAsBytes;
+ if (StringUtils.isBlank(certPath)) {
+ return Optional.empty();
+ }
+ try {
+ byte[] bytes = copyFileFromContainer(certPath, IOUtils::toByteArray);
+ if (bytes.length > 0) {
+ return Optional.of(bytes);
+ }
+ } catch (NotFoundException e) {
+ // just emit an error message, but do not throw an exception
+ // this might be ok, if the docker image is accidentally looking like version 8 or latest
+ // can happen if Elasticsearch is repackaged, i.e. with custom plugins
+ log.warn("CA cert under " + certPath + " not found.");
+ }
+ return Optional.empty();
}
/**
- * A SSL context based on the self signed CA, so that using this SSL Context allows to connect to the Elasticsearch service
+ * A SSL context based on the self-signed CA, so that using this SSL Context allows to connect to the Elasticsearch service
* @return a customized SSL Context
*/
public SSLContext createSslContextFromCa() {
try {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
- Certificate trustedCa = factory.generateCertificate(new ByteArrayInputStream(caCertAsBytes.get()));
+ Certificate trustedCa = factory.generateCertificate(
+ new ByteArrayInputStream(
+ caCertAsBytes()
+ .orElseThrow(() -> new IllegalStateException("CA cert under " + certPath + " not found."))
+ )
+ );
KeyStore trustStore = KeyStore.getInstance("pkcs12");
trustStore.load(null, null);
trustStore.setCertificateEntry("ca", trustedCa);
@@ -172,13 +183,13 @@ public SSLContext createSslContextFromCa() {
/**
* Define the Elasticsearch password to set. It enables security behind the scene for major version below 8.0.0.
* It's not possible to use security with the oss image.
- * @param password Password to set
+ * @param password Password to set
* @return this
*/
public ElasticsearchContainer withPassword(String password) {
if (isOss) {
throw new IllegalArgumentException(
- "You can not activate security on Elastic OSS Image. " + "Please switch to the default distribution"
+ "You can not activate security on Elastic OSS Image. Please switch to the default distribution"
);
}
withEnv("ELASTIC_PASSWORD", password);
@@ -200,11 +211,151 @@ public ElasticsearchContainer withCertPath(String certPath) {
return this;
}
+ @Override
+ public ElasticsearchContainer withStartupTimeout(Duration startupTimeout) {
+ this.healthCheckTimeout = startupTimeout;
+ if (getWaitStrategy() != null) {
+ getWaitStrategy().withStartupTimeout(startupTimeout);
+ }
+ return self();
+ }
+
+ String getCertPath() {
+ return certPath;
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+ configureWaitStrategy();
+ }
+
+ private void configureWaitStrategy() {
+ if (getWaitStrategy() != null) {
+ return;
+ }
+ setWaitStrategy(
+ new AbstractWaitStrategy() {
+ @Override
+ protected void waitUntilReady() {
+ String password = getEnvMap().get("ELASTIC_PASSWORD");
+ // Wait for port 9200 to accept TCP connections first, so that
+ // getHttpScheme()'s curl probe always finds a live socket and no
+ // version-based heuristics are needed.
+ // Track the deadline so both steps share the same total timeout.
+ Instant deadline = Instant.now().plus(startupTimeout);
+ Wait.forListeningPort().withStartupTimeout(startupTimeout).waitUntilReady(waitStrategyTarget);
+ Duration remaining = Duration.between(Instant.now(), deadline);
+ HttpWaitStrategy inner = "https".equals(getHttpScheme())
+ ? Wait.forHttps("/_cluster/health").forPort(ELASTICSEARCH_DEFAULT_PORT).allowInsecure()
+ : Wait.forHttp("/_cluster/health").forPort(ELASTICSEARCH_DEFAULT_PORT);
+ if (password != null) {
+ inner = inner.withBasicCredentials("elastic", password);
+ }
+ inner
+ .forStatusCode(200)
+ .forResponsePredicate(body -> {
+ return body.contains("\"status\":\"green\"") || body.contains("\"status\":\"yellow\"");
+ })
+ .withStartupTimeout(remaining.isNegative() ? Duration.ZERO : remaining)
+ .waitUntilReady(waitStrategyTarget);
+ }
+ }
+ .withStartupTimeout(healthCheckTimeout)
+ );
+ }
+
public String getHttpHostAddress() {
return getHost() + ":" + getMappedPort(ELASTICSEARCH_DEFAULT_PORT);
}
- @Deprecated // The TransportClient will be removed in Elasticsearch 8. No need to expose this port anymore in the future.
+ /**
+ * Detects the HTTP scheme used by Elasticsearch. Respects explicit env-var config first;
+ * when ambiguous, probes the live socket with curl (requires a running container on port 9200).
+ *
+ * @return "http" or "https"
+ */
+ String getHttpScheme() {
+ String securityEnabled = getEnvMap().get("xpack.security.enabled");
+ String httpSslEnabled = getEnvMap().get("xpack.security.http.ssl.enabled");
+
+ // Respect explicit user config
+ if ("false".equalsIgnoreCase(securityEnabled) || "false".equalsIgnoreCase(httpSslEnabled)) {
+ return "http";
+ }
+ if ("true".equalsIgnoreCase(httpSslEnabled)) {
+ return "https";
+ }
+
+ if (!isRunning()) {
+ throw new IllegalStateException(
+ "Cannot determine HTTP scheme: environment variables are not set and container is not running for curl probe"
+ );
+ }
+
+ ExecResult httpsResult = null;
+ ExecResult httpResult = null;
+ try {
+ // HTTPS probe: any HTTP response (200/401/403/...) => scheme is HTTPS.
+ // http_code == 000 means we didn't get an HTTP response (TLS/connect failure/timeout).
+ httpsResult =
+ execInContainer(
+ "curl",
+ "-sS",
+ "-k",
+ "--connect-timeout",
+ "2",
+ "--max-time",
+ "4",
+ "-o",
+ "/dev/null",
+ "-w",
+ "%{http_code}",
+ "https://localhost:" + ELASTICSEARCH_DEFAULT_PORT + "/"
+ );
+ if (httpsResult.getExitCode() == 0 && !"000".equals(httpsResult.getStdout().trim())) {
+ return "https";
+ }
+
+ // HTTP probe
+ httpResult =
+ execInContainer(
+ "curl",
+ "-sS",
+ "--connect-timeout",
+ "2",
+ "--max-time",
+ "4",
+ "-o",
+ "/dev/null",
+ "-w",
+ "%{http_code}",
+ "http://localhost:" + ELASTICSEARCH_DEFAULT_PORT + "/"
+ );
+ if (httpResult.getExitCode() == 0 && !"000".equals(httpResult.getStdout().trim())) {
+ return "http";
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to detect protocol via curl", e);
+ }
+
+ throw new RuntimeException(
+ String.format(
+ "Failed to detect protocol via curl. Both HTTPS and HTTP probes failed. " +
+ "HTTPS probe - exit code: %d, stdout: %s, stderr: %s; " +
+ "HTTP probe - exit code: %d, stdout: %s, stderr: %s",
+ httpsResult.getExitCode(),
+ httpsResult.getStdout(),
+ httpsResult.getStderr(),
+ httpResult.getExitCode(),
+ httpResult.getStdout(),
+ httpResult.getStderr()
+ )
+ );
+ }
+
+ // The TransportClient will be removed in Elasticsearch 8. No need to expose this port anymore in the future.
+ @Deprecated
public InetSocketAddress getTcpHost() {
return new InetSocketAddress(getHost(), getMappedPort(ELASTICSEARCH_DEFAULT_TCP_PORT));
}
diff --git a/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/KibanaContainer.java b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/KibanaContainer.java
new file mode 100644
index 00000000000..ee8ce714854
--- /dev/null
+++ b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/KibanaContainer.java
@@ -0,0 +1,669 @@
+package org.testcontainers.elasticsearch;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.github.dockerjava.api.model.ContainerNetwork;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.rnorth.ducttape.unreliables.Unreliables;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.DockerClientFactory;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.images.builder.Transferable;
+import org.testcontainers.utility.Base58;
+import org.testcontainers.utility.ComparableVersion;
+import org.testcontainers.utility.DockerImageName;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Testcontainers implementation for Kibana.
+ * Minimum supported version: {@value MINIMUM_SUPPORTED_VERSION}
+ *
+ * Supports two modes:
+ *
+ * Managed mode: Kibana automatically connects to an {@link ElasticsearchContainer}.
+ * See KibanaContainerTest#managedModeCanStartAndReachElasticsearchInSameExplicitNetwork()
+ * External mode: Kibana connects to an external Elasticsearch instance via URL.
+ * See KibanaContainerTest#externalModeCanWorkWithUsernamePassword()
+ *
+ *
+ */
+public class KibanaContainer extends GenericContainer {
+
+ private static final String ES_CA_CERT_PATH = "/usr/share/kibana/config/certs/es-ca.crt";
+
+ private static final Logger log = LoggerFactory.getLogger(KibanaContainer.class);
+
+ private static final int KIBANA_DEFAULT_PORT = 5601;
+
+ private static final String KIBANA_SYSTEM_USER = "kibana_system";
+
+ private static final String MINIMUM_SUPPORTED_VERSION = "8.0.0";
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("docker.elastic.co/kibana/kibana");
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private String encryptionKey;
+
+ private ElasticsearchContainer elasticsearch;
+
+ private String elasticsearchUrl;
+
+ private String elasticsearchUsername;
+
+ private String elasticsearchPassword;
+
+ private String elasticsearchServiceAccountToken;
+
+ private byte[] elasticsearchCaCertificate;
+
+ private Duration startupTimeout = Duration.ofSeconds(120);
+
+ /**
+ * If KibanaContainer creates an ad-hoc shared network (managed mode, neither container has an explicit network),
+ * it owns closing it.
+ */
+ private Network createdSharedNetwork;
+
+ /**
+ * Creates a KibanaContainer in managed mode.
+ * Kibana automatically connects to the provided Elasticsearch container.
+ *
+ * @param elasticsearch the Elasticsearch container to connect to
+ */
+ public KibanaContainer(ElasticsearchContainer elasticsearch) {
+ this(buildDockerImageName(elasticsearch));
+ this.elasticsearch = elasticsearch;
+ dependsOn(elasticsearch);
+ }
+
+ /**
+ * Creates a KibanaContainer in external mode.
+ * Use {@link #withElasticsearchUrl(String)} to configure the Elasticsearch connection. Use other methods to provide security credentials and such.
+ *
+ * @param dockerImageName the Docker image name
+ */
+ public KibanaContainer(String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ /**
+ * Creates a KibanaContainer in external mode.
+ * Use {@link #withElasticsearchUrl(String)} to configure the Elasticsearch connection. Use other methods to provide security credentials and such.
+ *
+ * @param dockerImageName the Docker image name
+ */
+ public KibanaContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ ensureCompatibleVersion(dockerImageName.getVersionPart());
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ this.encryptionKey = stableConfigKey(dockerImageName);
+
+ withExposedPorts(KIBANA_DEFAULT_PORT);
+ //we have to explicitly set wait the strategy later on in configure, once we know the security configuration
+ setWaitStrategy(null);
+ }
+
+ /**
+ * Sets the encryption key used for Kibana's encrypted saved objects.
+ * The key must be at least 32 characters. When not set, a deterministic default derived from
+ * the image name is used, which is required for {@link #withReuse(boolean)} to work correctly.
+ *
+ * @param encryptionKey the encryption key
+ * @return this container instance
+ */
+ public KibanaContainer withEncryptionKey(String encryptionKey) {
+ if (encryptionKey == null || encryptionKey.length() < 32) {
+ throw new IllegalArgumentException("Kibana encryption key must be at least 32 characters long");
+ }
+ this.encryptionKey = encryptionKey;
+ return this;
+ }
+
+ /**
+ * Enables or disables container reuse across JVM runs.
+ *
+ * Supported in external mode only. When Kibana is configured via
+ * {@link #withElasticsearchUrl(String)}, the container configuration is fully deterministic
+ * and TC can reliably locate the running container on subsequent runs.
+ *
+ *
Reuse is not supported in managed mode (i.e. when this container was created with
+ * an {@link ElasticsearchContainer}). Managed mode introduces several non-deterministic inputs
+ * into the container hash on every run (ad-hoc network ID, random network alias, fresh service
+ * account token), so TC always sees a different hash and starts a fresh container instead of
+ * reusing the existing one. This is a framework-level characteristic that affects any container
+ * connected via {@code withNetwork()} — not specific to {@code KibanaContainer}.
+ *
+ * @param reusable whether to enable container reuse
+ * @return this container instance
+ * @throws IllegalStateException if {@code reusable} is {@code true} and managed mode is active
+ */
+ @Override
+ public KibanaContainer withReuse(boolean reusable) {
+ if (reusable && elasticsearch != null) {
+ throw new IllegalStateException(
+ "withReuse(true) is not supported for KibanaContainer in managed mode. " +
+ "Use external mode (withElasticsearchUrl) to enable reuse."
+ );
+ }
+ return super.withReuse(reusable);
+ }
+
+ /**
+ * Configures the Elasticsearch URL for external mode.
+ *
+ * @param elasticsearchUrl the Elasticsearch URL (e.g., "https://my.fancy.setup.elastic.cloud:9200")
+ * @return this container instance
+ * @throws IllegalStateException if already using managed mode
+ */
+ public KibanaContainer withElasticsearchUrl(String elasticsearchUrl) {
+ if (elasticsearch != null) {
+ throw new IllegalStateException("Cannot set Elasticsearch URL when using Elasticsearch container");
+ }
+ this.elasticsearchUrl = elasticsearchUrl;
+ return this;
+ }
+
+ /**
+ * Configures Kibana to authenticate using the kibana_system user.
+ *
+ * @param password the password for the kibana_system user
+ * @return this container instance
+ */
+ public KibanaContainer withKibanaSystemPassword(String password) {
+ return withKibanaUsernameAndPassword(KIBANA_SYSTEM_USER, password);
+ }
+
+ /**
+ * Configures credentials Kibana will use for authentication.
+ *
+ * @param username the Elasticsearch username (cannot be 'elastic')
+ * @param password the password
+ * @return this container instance
+ * @throws IllegalStateException if a service account token is already configured
+ * @throws IllegalArgumentException if credentials are invalid
+ */
+ public KibanaContainer withKibanaUsernameAndPassword(String username, String password) {
+ if (elasticsearchServiceAccountToken != null) {
+ throw new IllegalStateException(
+ "Conflicting Elasticsearch credentials: provide either a service account token " +
+ "or a username/password pair, not both."
+ );
+ }
+ if (StringUtils.isAnyBlank(username, password)) {
+ throw new IllegalArgumentException("Kibana credentials cannot be blank");
+ }
+ if (!username.equals(username.trim()) || !password.equals(password.trim())) {
+ throw new IllegalArgumentException("Kibana credentials cannot have leading or trailing whitespace");
+ }
+ if ("elastic".equals(username)) {
+ throw new IllegalArgumentException("Username 'elastic' is reserved for internal use by Elasticsearch");
+ }
+
+ this.elasticsearchUsername = username;
+ this.elasticsearchPassword = password;
+ return this;
+ }
+
+ /**
+ * Configures a service account token for Elasticsearch authentication.
+ *
+ * @param token the service account token
+ * @return this container instance
+ * @throws IllegalStateException if username/password credentials are already configured
+ * @throws IllegalArgumentException if token is blank
+ */
+ public KibanaContainer withElasticsearchServiceAccountToken(String token) {
+ if (elasticsearchUsername != null) {
+ throw new IllegalStateException(
+ "Conflicting Elasticsearch credentials: provide either a service account token " +
+ "or a username/password pair, not both."
+ );
+ }
+ if (StringUtils.isBlank(token)) {
+ throw new IllegalArgumentException("Service account token cannot be empty");
+ }
+
+ if (!token.equals(token.trim())) {
+ throw new IllegalArgumentException("Service token cannot have leading or trailing whitespace");
+ }
+ this.elasticsearchServiceAccountToken = token;
+ return this;
+ }
+
+ /**
+ * Configures the Elasticsearch CA certificate for HTTPS connections.
+ *
+ * @param caCertificate the CA certificate in PEM format
+ * @return this container instance
+ * @throws IllegalArgumentException if certificate is empty
+ */
+ public KibanaContainer withElasticsearchCaCertificate(byte[] caCertificate) {
+ if (caCertificate == null || caCertificate.length == 0) {
+ throw new IllegalArgumentException("Elasticsearch CA certificate cannot be empty");
+ }
+ this.elasticsearchCaCertificate = caCertificate;
+ return this;
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+
+ addEnv("XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY", encryptionKey);
+ addEnv("SERVER_NAME", "kibana");
+
+ if (elasticsearchCaCertificate != null) {
+ withCopyToContainer(Transferable.of(elasticsearchCaCertificate), ES_CA_CERT_PATH);
+ addEnv("ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES", ES_CA_CERT_PATH);
+ }
+ if (elasticsearch != null) {
+ configureManagedElasticsearch();
+ } else if (elasticsearchUrl != null) {
+ configureExternalElasticsearch();
+ } else {
+ throw new IllegalStateException(
+ "Elasticsearch must be configured either via constructor KibanaContainer(elasticsearch) " +
+ "or via .withElasticsearchUrl() for external Elasticsearch"
+ );
+ }
+ //wait strategy is set in configure, because we don't know the security configuration before
+ configureWaitStrategy();
+ }
+
+ @Override
+ protected void containerIsStarted(InspectContainerResponse containerInfo) {
+ super.containerIsStarted(containerInfo);
+ log.info("Kibana is now ready, it can be accessed at http://{}", getHttpHostAddress());
+ }
+
+ @Override
+ public void stop() {
+ super.stop();
+ if (createdSharedNetwork != null) {
+ try {
+ createdSharedNetwork.close();
+ } catch (Exception e) {
+ log.debug("Failed to close shared network", e);
+ } finally {
+ createdSharedNetwork = null;
+ }
+ }
+ }
+
+ @Override
+ public KibanaContainer withStartupTimeout(Duration startupTimeout) {
+ this.startupTimeout = startupTimeout;
+ return this;
+ }
+
+ private static DockerImageName buildDockerImageName(ElasticsearchContainer elasticsearch) {
+ String esVersion = DockerImageName.parse(elasticsearch.getDockerImageName()).getVersionPart();
+ ensureCompatibleVersion(esVersion);
+ return DEFAULT_IMAGE_NAME.withTag(esVersion);
+ }
+
+ private void configureExternalElasticsearch() {
+ addEnv("ELASTICSEARCH_HOSTS", elasticsearchUrl);
+ if (elasticsearchServiceAccountToken != null) {
+ addEnv("ELASTICSEARCH_SERVICEACCOUNTTOKEN", elasticsearchServiceAccountToken);
+ } else if (elasticsearchUsername != null && elasticsearchPassword != null) {
+ addEnv("ELASTICSEARCH_USERNAME", elasticsearchUsername);
+ addEnv("ELASTICSEARCH_PASSWORD", elasticsearchPassword);
+ } else {
+ log.info(
+ "No Elasticsearch credentials provided for external mode; Kibana will attempt to connect anonymously"
+ );
+ }
+ }
+
+ private void configureManagedElasticsearch() {
+ ensureCorrectNetworkSetupForManagedMode();
+
+ if (getNetwork() == null) {
+ createAdHocNetwork();
+ }
+
+ String protocol = elasticsearch.getHttpScheme();
+
+ String hosts = protocol + "://" + resolveExistingEsDnsNameOnNetwork(getNetwork()) + ":9200";
+ addEnv("ELASTICSEARCH_HOSTS", hosts);
+
+ if ("https".equals(protocol)) {
+ // In managed mode, if Elasticsearch uses HTTPS we must configure Kibana with the ES CA, unless provided by user
+ if (this.elasticsearchCaCertificate == null) {
+ byte[] ca = copyElasticsearchHttpCaCertificateOrThrow();
+
+ withCopyToContainer(Transferable.of(ca), ES_CA_CERT_PATH);
+ addEnv("ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES", ES_CA_CERT_PATH);
+ }
+ if (elasticsearch != null && !getEnvMap().containsKey("ELASTICSEARCH_SSL_VERIFICATIONMODE")) {
+ addEnv("ELASTICSEARCH_SSL_VERIFICATIONMODE", "certificate");
+ }
+ }
+
+ // Elasticsearch 8.x+ has the security enabled by default, so lack of the env var set to false means security is enabled
+ boolean securityDisabled = "false".equalsIgnoreCase(elasticsearch.getEnvMap().get("xpack.security.enabled"));
+
+ if (!securityDisabled) {
+ // Managed mode: authenticate Kibana -> Elasticsearch using a Kibana service account token.
+ // This avoids any password lifecycle management for kibana_system.
+ String token = createKibanaServiceAccountToken(protocol);
+ addEnv("ELASTICSEARCH_SERVICEACCOUNTTOKEN", token);
+ }
+ }
+
+ private byte[] copyElasticsearchHttpCaCertificateOrThrow() {
+ try {
+ return elasticsearch.copyFileFromContainer(elasticsearch.getCertPath(), IOUtils::toByteArray);
+ } catch (Exception e) {
+ throw new IllegalStateException(
+ "Failed to copy Elasticsearch HTTP CA certificate from '" +
+ elasticsearch.getCertPath() +
+ "'. " +
+ "In managed HTTPS mode, KibanaContainer requires access to the Elasticsearch HTTP CA.",
+ e
+ );
+ }
+ }
+
+ private void ensureCorrectNetworkSetupForManagedMode() {
+ Network esNetwork = elasticsearch.getNetwork();
+ Network kbNetwork = this.getNetwork();
+
+ if ((esNetwork == null) != (kbNetwork == null)) {
+ throw new IllegalStateException(
+ "Managed mode requires either both containers share the same explicit network, " +
+ "or neither specifies a network (KibanaContainer will create one). "
+ );
+ }
+
+ // Both explicit: must be same
+ if (esNetwork != kbNetwork) {
+ throw new IllegalStateException(
+ "Elasticsearch and Kibana have different networks configured. " +
+ "In managed mode both containers must share the same explicit network instance, " +
+ "or neither must define a network."
+ );
+ }
+ }
+
+ private void createAdHocNetwork() {
+ // Fully managed: create ad-hoc network and own it.
+ createdSharedNetwork = Network.newNetwork();
+ withNetwork(createdSharedNetwork);
+
+ // Managed-mode safety rule: by the time Kibana is configuring itself, Elasticsearch must already be
+ // started (via dependsOn)
+ String esId = requireElasticsearchContainerId();
+
+ // Elasticsearch is already created/started. Attach it to the ad-hoc network.
+ // We don't need to provide an explicit alias - we'll use the container name for DNS resolution.
+ // Equivalent of https://docs.docker.com/reference/cli/docker/network/connect/
+ connectRunningContainerToNetwork(esId, createdSharedNetwork);
+ }
+
+ private String resolveExistingEsDnsNameOnNetwork(Network network) {
+ String esId = requireElasticsearchContainerId();
+
+ InspectContainerResponse info = DockerClientFactory.instance().client().inspectContainerCmd(esId).exec();
+
+ Map networks = info.getNetworkSettings().getNetworks();
+ if (networks == null) {
+ throw new IllegalStateException("Elasticsearch container has no network configuration");
+ }
+
+ // Try to find the network endpoint - Docker may key by network name or ID
+ ContainerNetwork endpoint = findNetworkEndpoint(networks, network);
+ if (endpoint == null) {
+ throw new IllegalStateException(
+ "Elasticsearch container is not connected to the expected network. " +
+ "Ensure both containers use the same Network instance."
+ );
+ }
+
+ // Prefer user-defined network aliases (skip Testcontainers auto-generated tc-* aliases)
+ if (endpoint.getAliases() != null && !endpoint.getAliases().isEmpty()) {
+ for (String alias : endpoint.getAliases()) {
+ if (StringUtils.isNotBlank(alias)) {
+ String cleaned = alias.trim();
+ // Skip Testcontainers auto-generated aliases (tc-*), prefer user-defined ones
+ if (!cleaned.startsWith("tc-")) {
+ log.info("Using Elasticsearch network alias: {}", cleaned);
+ return cleaned;
+ }
+ }
+ }
+ }
+
+ // Fallback: use container name
+ String containerName = info.getName();
+ if (containerName != null && !containerName.trim().isEmpty()) {
+ String dnsName = containerName.replaceFirst("^/", "").trim();
+ log.info("No user-defined network alias found, using Elasticsearch container name: {}", dnsName);
+ return dnsName;
+ }
+
+ throw new IllegalStateException(
+ "Cannot determine Elasticsearch DNS name. " +
+ "When using a custom network, set a network alias on the Elasticsearch container."
+ );
+ }
+
+ private ContainerNetwork findNetworkEndpoint(Map networks, Network network) {
+ // Try network ID first
+ ContainerNetwork endpoint = networks.get(network.getId());
+ if (endpoint != null) {
+ return endpoint;
+ }
+
+ // Try network name as fallback (Docker may key by name instead of ID)
+ try {
+ String networkName = DockerClientFactory
+ .instance()
+ .client()
+ .inspectNetworkCmd()
+ .withNetworkId(network.getId())
+ .exec()
+ .getName();
+ if (networkName != null) {
+ return networks.get(networkName);
+ }
+ } catch (Exception e) {
+ // Ignore and return null
+ }
+
+ return null;
+ }
+
+ private String requireElasticsearchContainerId() {
+ String id = elasticsearch.getContainerId();
+ if (StringUtils.isBlank(id)) {
+ throw new IllegalStateException(
+ "Elasticsearch containerId is not available. In managed mode, Elasticsearch must be started via dependsOn(elasticsearch) " +
+ "before KibanaContainer is started."
+ );
+ }
+ return id.trim();
+ }
+
+ private void connectRunningContainerToNetwork(String containerId, Network network) {
+ String networkId = network.getId();
+
+ try {
+ DockerClientFactory
+ .instance()
+ .client()
+ .connectToNetworkCmd()
+ .withContainerId(containerId)
+ .withNetworkId(networkId)
+ .exec();
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to connect Elasticsearch container to ad-hoc shared network", e);
+ }
+ }
+
+ private static void ensureCompatibleVersion(String esVersion) {
+ ComparableVersion comparableVersion = new ComparableVersion(esVersion);
+ if (comparableVersion.isLessThan(MINIMUM_SUPPORTED_VERSION)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Kibana version %s is not supported. Minimum version is %s",
+ comparableVersion,
+ MINIMUM_SUPPORTED_VERSION
+ )
+ );
+ }
+ }
+
+ private String createKibanaServiceAccountToken(String protocol) {
+ if (elasticsearch == null) {
+ throw new IllegalStateException("Cannot create service account token in external mode");
+ }
+
+ String elasticPassword = elasticsearch
+ .getEnvMap()
+ .getOrDefault("ELASTIC_PASSWORD", ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD);
+
+ // Create a unique token name to avoid collisions if the same ES container is reused.
+ String tokenName = "tc-kibana-" + Base58.randomString(12);
+
+ String endpoint = protocol + "://localhost:9200/_security/service/elastic/kibana/credential/token/" + tokenName;
+
+ return Unreliables.retryUntilSuccess(
+ 45,
+ TimeUnit.SECONDS,
+ () -> {
+ String curlTlsArgs = "";
+ if ("https".equals(protocol)) {
+ // In managed HTTPS mode, use the Elasticsearch HTTP CA for curl.
+ curlTlsArgs = " --cacert '" + elasticsearch.getCertPath() + "'";
+ }
+
+ String curlCommand = String.format(
+ "curl -sS%s -u \"elastic:$1\" -H 'Content-Type: application/json' -X POST '%s'",
+ curlTlsArgs,
+ endpoint
+ );
+
+ Container.ExecResult result = elasticsearch.execInContainer(
+ "/bin/sh",
+ "-c",
+ curlCommand,
+ "sh",
+ elasticPassword
+ );
+
+ String stdout = (result.getStdout() == null) ? "" : result.getStdout();
+ String stderr = (result.getStderr() == null) ? "" : result.getStderr();
+
+ if (result.getExitCode() != 0) {
+ throw new RuntimeException(
+ "Failed to create Kibana service account token. Exit code: " +
+ result.getExitCode() +
+ ", stdout: " +
+ stdout +
+ ", stderr: " +
+ stderr
+ );
+ }
+
+ JsonNode json = OBJECT_MAPPER.readTree(stdout);
+ JsonNode value = json.path("token").path("value");
+ if (value.isTextual() && !value.asText().trim().isEmpty()) {
+ return value.asText().trim();
+ }
+
+ throw new RuntimeException("Service account token response did not contain token.value: " + stdout);
+ }
+ );
+ }
+
+ private static String stableConfigKey(DockerImageName imageName) {
+ // UUID v3 (name-based) gives a deterministic 32-character string from the image name,
+ // keeping xpack.encryptedSavedObjects.encryptionKey identical across JVM runs —
+ // a prerequisite for container reuse. Call withEncryptionKey() for real secret management.
+ return UUID
+ .nameUUIDFromBytes(imageName.asCanonicalNameString().getBytes(StandardCharsets.UTF_8))
+ .toString()
+ .replace("-", "");
+ }
+
+ /**
+ * Returns the HTTP host address for accessing Kibana.
+ *
+ * @return the host address in the format "host:port"
+ */
+ public String getHttpHostAddress() {
+ return getHost() + ":" + getMappedPort(KIBANA_DEFAULT_PORT);
+ }
+
+ private void configureWaitStrategy() {
+ if (this.getWaitStrategy() != null) {
+ // the user might have set a custom wait strategy
+ return;
+ }
+ HttpWaitStrategy strategy = Wait
+ .forHttp("/api/status")
+ .forPort(KIBANA_DEFAULT_PORT)
+ .forStatusCode(200)
+ .forResponsePredicate(this::isKibanaReady);
+
+ // Add authentication if we have Elasticsearch credentials available
+ String serviceToken = getEnvMap().get("ELASTICSEARCH_SERVICEACCOUNTTOKEN");
+ String username = getEnvMap().get("ELASTICSEARCH_USERNAME");
+ String password = getEnvMap().get("ELASTICSEARCH_PASSWORD");
+
+ if (serviceToken != null) {
+ strategy = strategy.withHeader("Authorization", "Bearer " + serviceToken);
+ } else if (username != null && password != null) {
+ strategy = strategy.withBasicCredentials(username, password);
+ }
+
+ setWaitStrategy(strategy.withStartupTimeout(this.startupTimeout));
+ }
+
+ private boolean isKibanaReady(String body) {
+ try {
+ JsonNode json = OBJECT_MAPPER.readTree(body);
+ JsonNode status = json.path("status");
+
+ String overallLevel = status.path("overall").path("level").asText(null);
+ String elasticsearchLevel = status.path("core").path("elasticsearch").path("level").asText(null);
+ String savedObjectsLevel = status.path("core").path("savedObjects").path("level").asText(null);
+
+ boolean overallAvailable = "available".equalsIgnoreCase(overallLevel);
+ boolean elasticsearchAvailable = "available".equalsIgnoreCase(elasticsearchLevel);
+ boolean savedObjectsAvailable = "available".equalsIgnoreCase(savedObjectsLevel);
+
+ boolean isReady = overallAvailable && elasticsearchAvailable && savedObjectsAvailable;
+
+ if (log.isDebugEnabled()) {
+ log.debug(
+ "Kibana status check: READY={} (overall={}, elasticsearch={}, savedObjects={})",
+ isReady,
+ overallLevel,
+ elasticsearchLevel,
+ savedObjectsLevel
+ );
+ }
+
+ return isReady;
+ } catch (Exception e) {
+ log.debug("Kibana status check: FAILED to parse response - {}", e.getMessage());
+ return false;
+ }
+ }
+}
diff --git a/modules/elasticsearch/src/main/resources/elasticsearch-default-memory-vm.options b/modules/elasticsearch/src/main/resources/elasticsearch-default-memory-vm.options
index 10db7b96157..62b4e57c6e1 100644
--- a/modules/elasticsearch/src/main/resources/elasticsearch-default-memory-vm.options
+++ b/modules/elasticsearch/src/main/resources/elasticsearch-default-memory-vm.options
@@ -1,2 +1,3 @@
-Xms2147483648
-Xmx2147483648
+-Dingest.geoip.downloader.enabled.default=false
diff --git a/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java b/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java
index 7ee81c82759..00494948f1d 100644
--- a/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java
+++ b/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java
@@ -16,13 +16,14 @@
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
-import org.junit.After;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.RemoteDockerImage;
+import org.testcontainers.images.builder.Transferable;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
@@ -33,7 +34,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
-public class ElasticsearchContainerTest {
+class ElasticsearchContainerTest {
/**
* Elasticsearch version which should be used for the Tests
@@ -44,6 +45,10 @@ public class ElasticsearchContainerTest {
.parse("docker.elastic.co/elasticsearch/elasticsearch")
.withTag(ELASTICSEARCH_VERSION);
+ private static final DockerImageName ELASTICSEARCH_LATEST_IMAGE = DockerImageName.parse(
+ "docker.elastic.co/elasticsearch/elasticsearch:9.2.4"
+ );
+
/**
* Elasticsearch default username, when secured
*/
@@ -58,7 +63,7 @@ public class ElasticsearchContainerTest {
private RestClient anonymousClient = null;
- @After
+ @AfterEach
public void stopRestClient() throws IOException {
if (client != null) {
client.close();
@@ -73,10 +78,10 @@ public void stopRestClient() throws IOException {
@SuppressWarnings("deprecation") // Using deprecated constructor for verification of backwards compatibility
@Test
@Deprecated // We will remove this test in the future
- public void elasticsearchDeprecatedCtorTest() throws IOException {
+ void elasticsearchDeprecatedCtorTest() throws IOException {
// Create the elasticsearch container.
try (
- ElasticsearchContainer container = new ElasticsearchContainer().withEnv("foo", "bar") // dummy env for compiler checking correct generics usage
+ ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE).withEnv("foo", "bar") // dummy env for compiler checking correct generics usage
) {
// Start the container. This step might take some time...
container.start();
@@ -95,7 +100,7 @@ public void elasticsearchDeprecatedCtorTest() throws IOException {
}
@Test
- public void elasticsearchDefaultTest() throws IOException {
+ void elasticsearchDefaultTest() throws IOException {
// Create the elasticsearch container.
try (
ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE).withEnv("foo", "bar") // dummy env for compiler checking correct generics usage
@@ -117,7 +122,7 @@ public void elasticsearchDefaultTest() throws IOException {
}
@Test
- public void elasticsearchSecuredTest() throws IOException {
+ void elasticsearchSecuredTest() throws IOException {
try (
ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)
.withPassword(ELASTICSEARCH_PASSWORD)
@@ -137,7 +142,7 @@ public void elasticsearchSecuredTest() throws IOException {
}
@Test
- public void elasticsearchVersion() throws IOException {
+ void elasticsearchVersion() throws IOException {
try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) {
container.start();
Response response = getClient(container).performRequest(new Request("GET", "/"));
@@ -148,7 +153,7 @@ public void elasticsearchVersion() throws IOException {
}
@Test
- public void elasticsearchVersion83() throws IOException {
+ void elasticsearchVersion83() throws IOException {
try (
ElasticsearchContainer container = new ElasticsearchContainer(
"docker.elastic.co/elasticsearch/elasticsearch:8.3.0"
@@ -162,13 +167,28 @@ public void elasticsearchVersion83() throws IOException {
}
@Test
- public void elasticsearchOssImage() throws IOException {
+ void clusterHealthIsAtLeastYellowAfterStart() throws IOException {
+ try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_LATEST_IMAGE)) {
+ container.start();
+
+ Response response = getClient(container).performRequest(new Request("GET", "/_cluster/health"));
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
+ String body = EntityUtils.toString(response.getEntity());
+ assertThat(body)
+ .as("Cluster health status should be at least yellow after container start")
+ .satisfiesAnyOf(
+ b -> assertThat(b).contains("\"status\":\"yellow\""),
+ b -> assertThat(b).contains("\"status\":\"green\"")
+ );
+ }
+ }
+
+ @Test
+ void elasticsearchOssImage() throws IOException {
try (
// ossContainer {
ElasticsearchContainer container = new ElasticsearchContainer(
- DockerImageName
- .parse("docker.elastic.co/elasticsearch/elasticsearch-oss")
- .withTag(ELASTICSEARCH_VERSION)
+ "docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2"
)
// }
) {
@@ -183,8 +203,8 @@ public void elasticsearchOssImage() throws IOException {
}
@Test
- public void restClientClusterHealth() throws IOException {
- // httpClientContainer {
+ void restClientClusterHealth() throws IOException {
+ // httpClientContainer7 {
// Create the elasticsearch container.
try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) {
// Start the container. This step might take some time...
@@ -209,13 +229,92 @@ public void restClientClusterHealth() throws IOException {
// }}
assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name");
- // httpClientContainer {{
+ // httpClientContainer7 {{
+ }
+ // }
+ }
+
+ @Test
+ void restClientClusterHealthElasticsearch8() throws IOException {
+ // httpClientContainer8 {
+ // Create the elasticsearch container.
+ try (
+ ElasticsearchContainer container = new ElasticsearchContainer(
+ "docker.elastic.co/elasticsearch/elasticsearch:8.1.2"
+ )
+ ) {
+ // Start the container. This step might take some time...
+ container.start();
+
+ // Do whatever you want with the rest client ...
+ final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ AuthScope.ANY,
+ new UsernamePasswordCredentials(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD)
+ );
+
+ client =
+ RestClient
+ // use HTTPS for Elasticsearch 8
+ .builder(HttpHost.create("https://" + container.getHttpHostAddress()))
+ .setHttpClientConfigCallback(httpClientBuilder -> {
+ httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
+ // SSL is activated by default in Elasticsearch 8
+ httpClientBuilder.setSSLContext(container.createSslContextFromCa());
+ return httpClientBuilder;
+ })
+ .build();
+
+ Response response = client.performRequest(new Request("GET", "/_cluster/health"));
+ // }}
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
+ assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name");
+ // httpClientContainer8 {{
}
// }
}
@Test
- public void restClientSecuredClusterHealth() throws IOException {
+ void restClientClusterHealthElasticsearch8WithoutSSL() throws IOException {
+ // httpClientContainerNoSSL8 {
+ // Create the elasticsearch container.
+ try (
+ ElasticsearchContainer container = new ElasticsearchContainer(
+ "docker.elastic.co/elasticsearch/elasticsearch:8.1.2"
+ )
+ // disable SSL
+ .withEnv("xpack.security.transport.ssl.enabled", "false")
+ .withEnv("xpack.security.http.ssl.enabled", "false")
+ ) {
+ // Start the container. This step might take some time...
+ container.start();
+
+ // Do whatever you want with the rest client ...
+ final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ AuthScope.ANY,
+ new UsernamePasswordCredentials(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD)
+ );
+
+ client =
+ RestClient
+ .builder(HttpHost.create(container.getHttpHostAddress()))
+ .setHttpClientConfigCallback(httpClientBuilder -> {
+ return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
+ })
+ .build();
+
+ Response response = client.performRequest(new Request("GET", "/_cluster/health"));
+ // }}
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
+ assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name");
+ // httpClientContainerNoSSL8 {{
+ }
+ // }
+ }
+
+ @Test
+ void restClientSecuredClusterHealth() throws IOException {
// httpClientSecuredContainer {
// Create the elasticsearch container.
try (
@@ -252,7 +351,7 @@ public void restClientSecuredClusterHealth() throws IOException {
@SuppressWarnings("deprecation") // The TransportClient will be removed in Elasticsearch 8.
@Test
- public void transportClientClusterHealth() {
+ void transportClientClusterHealth() {
// transportClientContainer {
// Create the elasticsearch container.
try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) {
@@ -278,15 +377,11 @@ public void transportClientClusterHealth() {
}
@Test
- public void incompatibleSettingsTest() {
+ void incompatibleSettingsTest() {
// The OSS image can not use security feature
assertThat(
catchThrowable(() -> {
- new ElasticsearchContainer(
- DockerImageName
- .parse("docker.elastic.co/elasticsearch/elasticsearch-oss")
- .withTag(ELASTICSEARCH_VERSION)
- )
+ new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2")
.withPassword("foo");
})
)
@@ -295,23 +390,16 @@ public void incompatibleSettingsTest() {
}
@Test
- public void testElasticsearch8SecureByDefault() throws Exception {
- try (
- ElasticsearchContainer container = new ElasticsearchContainer(
- "docker.elastic.co/elasticsearch/elasticsearch:8.1.2"
- )
- ) {
- // Start the container. This step might take some time...
+ void testDockerHubElasticsearch8ImageSecureByDefault() throws Exception {
+ try (ElasticsearchContainer container = new ElasticsearchContainer("elasticsearch:8.1.2")) {
container.start();
- Response response = getClusterHealth(container);
- assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
- assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name");
+ assertClusterHealthResponse(container);
}
}
@Test
- public void testElasticsearch8SecureByDefaultCustomCaCertFails() throws Exception {
+ void testElasticsearch8SecureByDefaultCustomCaCertFails() throws Exception {
final MountableFile mountableFile = MountableFile.forClasspathResource("http_ca.crt");
String caPath = "/tmp/http_ca.crt";
try (
@@ -333,7 +421,7 @@ public void testElasticsearch8SecureByDefaultCustomCaCertFails() throws Exceptio
}
@Test
- public void testElasticsearch8SecureByDefaultHttpWaitStrategy() throws Exception {
+ void testElasticsearch8SecureByDefaultHttpWaitStrategy() throws Exception {
final HttpWaitStrategy httpsWaitStrategy = Wait
.forHttps("/")
.forPort(9200)
@@ -351,14 +439,12 @@ public void testElasticsearch8SecureByDefaultHttpWaitStrategy() throws Exception
// Start the container. This step might take some time...
container.start();
- Response response = getClusterHealth(container);
- assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
- assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name");
+ assertClusterHealthResponse(container);
}
}
@Test
- public void testElasticsearch8SecureByDefaultFailsSilentlyOnLatestImages() throws Exception {
+ void testElasticsearch8SecureByDefaultFailsSilentlyOnLatestImages() throws Exception {
// this test exists for custom images by users that use the `latest` tag
// even though the version might be older than version 8
// this tags an old 7.x version as :latest
@@ -377,7 +463,50 @@ public void testElasticsearch8SecureByDefaultFailsSilentlyOnLatestImages() throw
}
@Test
- public void testElasticsearchDefaultMaxHeapSize() throws Exception {
+ void testElasticsearch7CanHaveSecurityEnabledAndUseSslContext() throws Exception {
+ String customizedCertPath = "/usr/share/elasticsearch/config/certs/http_ca_customized.crt";
+ try (
+ ElasticsearchContainer container = new ElasticsearchContainer(
+ "docker.elastic.co/elasticsearch/elasticsearch:7.17.15"
+ )
+ .withPassword(ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD)
+ .withEnv("xpack.security.enabled", "true")
+ .withEnv("xpack.security.http.ssl.enabled", "true")
+ .withEnv("xpack.security.http.ssl.key", "/usr/share/elasticsearch/config/certs/elasticsearch.key")
+ .withEnv(
+ "xpack.security.http.ssl.certificate",
+ "/usr/share/elasticsearch/config/certs/elasticsearch.crt"
+ )
+ .withEnv("xpack.security.http.ssl.certificate_authorities", customizedCertPath)
+ // these lines show how certificates can be created self-made way
+ // obviously this shouldn't be done in prod environment, where proper and officially signed keys should be present
+ .withCopyToContainer(
+ Transferable.of(
+ "#!/bin/bash\n" +
+ "mkdir -p /usr/share/elasticsearch/config/certs;" +
+ "openssl req -x509 -newkey rsa:4096 -keyout /usr/share/elasticsearch/config/certs/elasticsearch.key -out /usr/share/elasticsearch/config/certs/elasticsearch.crt -days 365 -nodes -subj \"/CN=localhost\";" +
+ "openssl x509 -outform der -in /usr/share/elasticsearch/config/certs/elasticsearch.crt -out " +
+ customizedCertPath +
+ "; chown -R elasticsearch /usr/share/elasticsearch/config/certs/",
+ 555
+ ),
+ "/usr/share/elasticsearch/generate-certs.sh"
+ )
+ // because we need to generate the certificates before Elasticsearch starts, the entry command has to be tuned accordingly
+ .withCommand(
+ "sh",
+ "-c",
+ "/usr/share/elasticsearch/generate-certs.sh && /usr/local/bin/docker-entrypoint.sh"
+ )
+ .withCertPath(customizedCertPath)
+ ) {
+ container.start();
+ assertClusterHealthResponse(container);
+ }
+ }
+
+ @Test
+ void testElasticsearchDefaultMaxHeapSize() throws Exception {
long defaultHeapSize = 2147483648L;
try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) {
@@ -387,7 +516,7 @@ public void testElasticsearchDefaultMaxHeapSize() throws Exception {
}
@Test
- public void testElasticsearchCustomMaxHeapSizeInEnvironmentVariable() throws Exception {
+ void testElasticsearchCustomMaxHeapSizeInEnvironmentVariable() throws Exception {
long customHeapSize = 1574961152;
try (
@@ -400,7 +529,7 @@ public void testElasticsearchCustomMaxHeapSizeInEnvironmentVariable() throws Exc
}
@Test
- public void testElasticsearchCustomMaxHeapSizeInJvmOptionsFile() throws Exception {
+ void testElasticsearchCustomMaxHeapSizeInJvmOptionsFile() throws Exception {
long customHeapSize = 1574961152;
try (
@@ -488,4 +617,43 @@ private void assertElasticsearchContainerHasHeapSize(ElasticsearchContainer cont
assertThat(responseBody).contains("\"heap_init_in_bytes\":" + heapSizeInBytes);
assertThat(responseBody).contains("\"heap_max_in_bytes\":" + heapSizeInBytes);
}
+
+ private void assertClusterHealthResponse(ElasticsearchContainer container) throws IOException {
+ Response response = getClusterHealth(container);
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
+ assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name");
+ }
+
+ @Test
+ void testGetHttpSchemeForElasticsearch7ReturnsHttp() {
+ try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) {
+ container.start();
+ assertThat(container.getHttpScheme()).isEqualTo("http");
+ }
+ }
+
+ @Test
+ void testGetHttpSchemeForElasticsearch8ReturnsHttps() {
+ try (
+ ElasticsearchContainer container = new ElasticsearchContainer(
+ "docker.elastic.co/elasticsearch/elasticsearch:8.1.2"
+ )
+ ) {
+ container.start();
+ assertThat(container.getHttpScheme()).isEqualTo("https");
+ }
+ }
+
+ @Test
+ void testGetHttpSchemeForElasticsearch8WithSslDisabledReturnsHttp() {
+ try (
+ ElasticsearchContainer container = new ElasticsearchContainer(
+ "docker.elastic.co/elasticsearch/elasticsearch:8.1.2"
+ )
+ .withEnv("xpack.security.http.ssl.enabled", "false")
+ ) {
+ container.start();
+ assertThat(container.getHttpScheme()).isEqualTo("http");
+ }
+ }
}
diff --git a/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/KibanaContainerTest.java b/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/KibanaContainerTest.java
new file mode 100644
index 00000000000..0099b5613ca
--- /dev/null
+++ b/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/KibanaContainerTest.java
@@ -0,0 +1,515 @@
+package org.testcontainers.elasticsearch;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.Testcontainers;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.ContainerLaunchException;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.images.builder.Transferable;
+import org.testcontainers.utility.TestcontainersConfiguration;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+class KibanaContainerTest {
+
+ public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private static final String ES_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:9.2.4";
+
+ @Test
+ void cannotCreateKibanaContainerForVersionLessThan8() {
+ Assertions
+ .assertThatThrownBy(() -> new KibanaContainer("docker.elastic.co/kibana/kibana:7.17.29"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("is not supported");
+ }
+
+ @Test
+ void managedModeCanStartAndReachElasticsearchInSameExplicitNetwork() throws IOException {
+ // managedModeCanStartAndReachElasticsearchInSameExplicitNetwork {
+ try (
+ Network network = Network.newNetwork();
+ ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE).withNetwork(network);
+ KibanaContainer kibana = new KibanaContainer(es).withNetwork(network)
+ ) {
+ es.start();
+ kibana.start();
+
+ String status = getKibanaStatus(kibana);
+ Assertions.assertThat(status).isEqualTo("available");
+ }
+ // }
+ }
+
+ @Test
+ void managedModeCannotStartWithOnlyESNetworkExplicit() {
+ Network network = Network.newNetwork();
+ try (
+ ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE).withNetwork(network);
+ KibanaContainer kibana = new KibanaContainer(es)
+ ) {
+ Assertions
+ .assertThatThrownBy(kibana::start)
+ .isInstanceOf(ContainerLaunchException.class)
+ .satisfies(ex -> {
+ Assertions
+ .assertThat(ex.getCause())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("explicit network");
+ });
+ }
+ }
+
+ @Test
+ void managedModeCannotStartWithDifferentExplicitNetworks() {
+ try (
+ Network esNetwork = Network.newNetwork();
+ Network kibanaNetwork = Network.newNetwork();
+ ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE).withNetwork(esNetwork);
+ KibanaContainer kibana = new KibanaContainer(es).withNetwork(kibanaNetwork)
+ ) {
+ Assertions
+ .assertThatThrownBy(kibana::start)
+ .isInstanceOf(ContainerLaunchException.class)
+ .satisfies(ex -> {
+ Assertions
+ .assertThat(ex.getCause())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("different networks");
+ });
+ }
+ }
+
+ @Test
+ void managedModeUsesCustomNetworkAliasInExplicitNetwork() throws Exception {
+ final String customEsAlias = "my-custom-es-alias";
+
+ try (
+ Network network = Network.newNetwork();
+ ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE)
+ .withNetwork(network)
+ .withNetworkAliases(customEsAlias);
+ KibanaContainer kibana = new KibanaContainer(es).withNetwork(network)
+ ) {
+ kibana.start();
+
+ Assertions.assertThat(kibana.isRunning()).isTrue();
+
+ // Verify Kibana uses the custom alias (not auto-generated tc-* alias)
+ Container.ExecResult result = kibana.execInContainer("sh", "-c", "env | grep ELASTICSEARCH_HOSTS");
+
+ Assertions.assertThat(result.getStdout()).contains(customEsAlias).contains(":9200");
+ }
+ }
+
+ @Test
+ void managedModeCanStartAndReachElasticsearchWithoutExplicitNetwork() throws IOException {
+ try (
+ ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE);
+ KibanaContainer kibana = new KibanaContainer(es)
+ ) {
+ kibana.start();
+
+ String status = getKibanaStatus(kibana);
+ Assertions.assertThat(status).isEqualTo("available");
+ }
+ }
+
+ @Test
+ void managedModeCanStartWithoutElasticsearchSecurity() throws IOException {
+ try (
+ ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE).withEnv("xpack.security.enabled", "false");
+ KibanaContainer kibana = new KibanaContainer(es)
+ ) {
+ kibana.start();
+
+ String status = getKibanaStatus(kibana);
+ Assertions.assertThat(status).isEqualTo("available");
+ }
+ }
+
+ @Test
+ void managedModeCanStartWithoutElasticsearchHttps() throws IOException {
+ try (
+ ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE)
+ .withEnv("xpack.security.enabled", "true")
+ .withEnv("xpack.security.http.ssl.enabled", "false");
+ KibanaContainer kibana = new KibanaContainer(es)
+ ) {
+ es.start();
+ kibana.start();
+
+ String status = getKibanaStatus(kibana);
+ Assertions.assertThat(status).isEqualTo("available");
+ }
+ }
+
+ @Test
+ void externalModeFailsWithConflictingCredentials() {
+ Assertions
+ .assertThatThrownBy(() -> {
+ new KibanaContainer("docker.elastic.co/kibana/kibana:8.0.0")
+ .withKibanaUsernameAndPassword("user", "pass")
+ .withElasticsearchServiceAccountToken("token");
+ })
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Conflicting Elasticsearch credentials");
+ }
+
+ @Test
+ void managedModeFailsWhenSettingElasticsearchUrl() {
+ ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE);
+ Assertions
+ .assertThatThrownBy(() -> {
+ new KibanaContainer(es).withElasticsearchUrl("http://somewhere.over.the.rainbow:9200");
+ })
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Cannot set Elasticsearch URL when using Elasticsearch container");
+ }
+
+ @Test
+ void failsWhenNoElasticsearchConfigured() {
+ try (KibanaContainer kibana = new KibanaContainer("docker.elastic.co/kibana/kibana:8.0.0")) {
+ Assertions
+ .assertThatThrownBy(kibana::start)
+ .isInstanceOf(ContainerLaunchException.class)
+ .satisfies(ex -> {
+ Assertions
+ .assertThat(ex.getCause())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Elasticsearch must be configured");
+ });
+ }
+ }
+
+ @Test
+ void externalModeCanWorkWithUsernamePassword() throws IOException, InterruptedException {
+ final String esHostname = "elasticsearch";
+
+ // externalModeCanWorkWithUsernamePassword {
+ try (
+ Network network = Network.newNetwork();
+ ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE)
+ .withNetwork(network)
+ .withNetworkAliases(esHostname)
+ .withEnv("xpack.security.http.ssl.enabled", "false")
+ ) {
+ es.start();
+ String kibanaSystemPassword = setKibanaSystemPassword(es);
+
+ try (
+ KibanaContainer kibana = new KibanaContainer("docker.elastic.co/kibana/kibana:9.2.2") //this minor version is intentionally below ES version
+ .withNetwork(network)
+ .withElasticsearchUrl("http://" + esHostname + ":9200")
+ .withKibanaSystemPassword(kibanaSystemPassword)
+ ) {
+ kibana.start();
+ String status = getKibanaStatus(kibana);
+ Assertions.assertThat(status).isEqualTo("available");
+ }
+ }
+ // }
+ }
+
+ @Test
+ void externalModeCanWorkWithoutCredentials() throws IOException {
+ final String esHostname = "elasticsearch";
+
+ try (
+ Network network = Network.newNetwork();
+ ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE)
+ .withNetwork(network)
+ .withNetworkAliases(esHostname)
+ .withEnv("xpack.security.enabled", "false")
+ .withEnv("xpack.security.http.ssl.enabled", "false");
+ KibanaContainer kibana = new KibanaContainer("docker.elastic.co/kibana/kibana:9.2.4")
+ .withNetwork(network)
+ .withElasticsearchUrl("http://" + esHostname + ":9200")
+ ) {
+ es.start();
+ kibana.start();
+ String status = getKibanaStatus(kibana);
+ Assertions.assertThat(status).isEqualTo("available");
+ }
+ }
+
+ @Test
+ void externalModeCanStartAndReachElasticsearchWithCertAndServiceToken() throws Exception {
+ byte[] caCrt;
+ byte[] nodeCrt;
+ byte[] nodeKey;
+
+ String esHostname = "elasticsearch";
+
+ String instancesYml =
+ "instances:\n" +
+ " - name: es01\n" +
+ " dns: [ \"localhost\", \"" +
+ esHostname +
+ "\", \"es01\" ]\n" +
+ " ip: [ \"127.0.0.1\" ]\n";
+
+ try (
+ ElasticsearchContainer setup = new ElasticsearchContainer(ES_IMAGE)
+ .withEnv("discovery.type", "single-node")
+ .withCopyToContainer(
+ Transferable.of(instancesYml.getBytes(StandardCharsets.UTF_8), 0644),
+ "/tmp/instances.yml"
+ )
+ ) {
+ setup.start();
+
+ // Run certutil inside the running container and write outputs inside the container FS
+ Container.ExecResult execResult = setup.execInContainer(
+ "bash",
+ "-lc",
+ "set -euo pipefail && " +
+ "mkdir -p /tmp/out && " +
+ "cd /usr/share/elasticsearch && " +
+ "bin/elasticsearch-certutil ca --silent --pem --out /tmp/out/ca.zip && " +
+ "unzip -o /tmp/out/ca.zip -d /tmp/out && " +
+ "bin/elasticsearch-certutil cert --silent --pem --in /tmp/instances.yml --ca-cert /tmp/out/ca/ca.crt --ca-key /tmp/out/ca/ca.key --out /tmp/out/certs.zip && " +
+ "unzip -o /tmp/out/certs.zip -d /tmp/out"
+ );
+ Assertions.assertThat(execResult.getExitCode()).isEqualTo(0);
+
+ // copy the certificates and key from the container, so we can use them later
+ caCrt = setup.copyFileFromContainer("/tmp/out/ca/ca.crt", IOUtils::toByteArray);
+ nodeCrt = setup.copyFileFromContainer("/tmp/out/es01/es01.crt", IOUtils::toByteArray);
+ nodeKey = setup.copyFileFromContainer("/tmp/out/es01/es01.key", IOUtils::toByteArray);
+ }
+
+ try (
+ Network network = Network.newNetwork();
+ ElasticsearchContainer es = new ElasticsearchContainer(
+ "docker.elastic.co/elasticsearch/elasticsearch:9.2.4"
+ )
+ .withNetwork(network)
+ .withNetworkAliases(esHostname)
+ ) {
+ applyTls(es, caCrt, nodeCrt, nodeKey);
+ es.start();
+ String kibanaServiceAccountToken = createKibanaServiceAccountToken(es);
+
+ try (
+ KibanaContainer kibana = new KibanaContainer("docker.elastic.co/kibana/kibana:9.2.4")
+ // network is needed only because the ES we try to access via explicit mode is operated by non-public Docker
+ .withNetwork(network)
+ .withElasticsearchUrl("https://" + esHostname + ":9200")
+ .withElasticsearchServiceAccountToken(kibanaServiceAccountToken)
+ .withElasticsearchCaCertificate(es.caCertAsBytes().get())
+ ) {
+ kibana.start();
+ String status = getKibanaStatus(kibana);
+ Assertions.assertThat(status).isEqualTo("available");
+ }
+ }
+ }
+
+ private static String setKibanaSystemPassword(ElasticsearchContainer elasticsearch) throws IOException {
+ String kibanaPassword = "kibana-system-" + System.currentTimeMillis();
+
+ try (CloseableHttpClient httpClient = createHttpClient(elasticsearch)) {
+ String url = String.format(
+ "%s://%s/_security/user/kibana_system/_password",
+ elasticsearch.getHttpScheme(),
+ elasticsearch.getHttpHostAddress()
+ );
+ HttpPost request = new HttpPost(url);
+ request.setHeader("Content-Type", "application/json");
+ request.setEntity(new StringEntity("{\"password\":\"" + kibanaPassword + "\"}"));
+
+ HttpResponse response = httpClient.execute(request);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String body = EntityUtils.toString(response.getEntity());
+
+ if (statusCode != 200) {
+ throw new IllegalStateException(
+ "Failed to set kibana_system password. HTTP " + statusCode + ", body=" + body
+ );
+ }
+
+ // ES 9.x returns {} on success; older versions may return {"acknowledged":true}
+ // Just validate that the body is valid JSON.
+ try {
+ OBJECT_MAPPER.readTree(body.isEmpty() ? "{}" : body);
+ } catch (IOException e) {
+ throw new IllegalStateException("Non-JSON response body: " + body, e);
+ }
+
+ return kibanaPassword;
+ }
+ }
+
+ private static String createKibanaServiceAccountToken(ElasticsearchContainer elasticsearch) throws IOException {
+ String tokenName = "kibana-token-" + System.currentTimeMillis();
+
+ try (CloseableHttpClient httpClient = createHttpClient(elasticsearch)) {
+ String url = String.format(
+ "%s://%s/_security/service/elastic/kibana/credential/token/%s",
+ elasticsearch.getHttpScheme(),
+ elasticsearch.getHttpHostAddress(),
+ tokenName
+ );
+ HttpPost request = new HttpPost(url);
+ request.setHeader("Content-Type", "application/json");
+
+ HttpResponse response = httpClient.execute(request);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String body = EntityUtils.toString(response.getEntity());
+
+ if (statusCode != 200) {
+ throw new IllegalStateException(
+ "Failed to create Kibana service account token. HTTP " + statusCode + ", body=" + body
+ );
+ }
+
+ // Expected JSON:
+ // {"created":true,"token":{"name":"...","value":"AAEAA..."}}
+ try {
+ JsonNode root = OBJECT_MAPPER.readTree(body);
+ JsonNode tokenValue = root.path("token").path("value");
+
+ if (tokenValue.isMissingNode() || tokenValue.isNull()) {
+ throw new IllegalStateException("Token value not found in response: " + body);
+ }
+
+ return tokenValue.asText();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to parse token response: " + body, e);
+ }
+ }
+ }
+
+ private static CloseableHttpClient createHttpClient(ElasticsearchContainer elasticsearch) {
+ String elasticPassword = elasticsearch.getEnvMap().get("ELASTIC_PASSWORD");
+ HttpClientBuilder clientBuilder = HttpClientBuilder.create();
+
+ if (StringUtils.isNotBlank(elasticPassword)) {
+ CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ AuthScope.ANY,
+ new UsernamePasswordCredentials("elastic", elasticPassword)
+ );
+ clientBuilder.setDefaultCredentialsProvider(credentialsProvider);
+ }
+
+ String scheme = elasticsearch.getHttpScheme();
+ if ("https".equals(scheme)) {
+ SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
+ elasticsearch.createSslContextFromCa()
+ );
+ clientBuilder.setSSLSocketFactory(sslSocketFactory);
+ }
+
+ return clientBuilder.build();
+ }
+
+ private static String getKibanaStatus(KibanaContainer kibana) throws IOException {
+ try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
+ String url = "http://" + kibana.getHttpHostAddress() + "/api/status";
+ HttpResponse response = httpClient.execute(new org.apache.http.client.methods.HttpGet(url));
+ int statusCode = response.getStatusLine().getStatusCode();
+ String body = EntityUtils.toString(response.getEntity());
+
+ if (statusCode != 200) {
+ throw new IllegalStateException("Failed to get Kibana status. HTTP " + statusCode + ", body=" + body);
+ }
+
+ JsonNode json = OBJECT_MAPPER.readTree(body);
+ String status = json.path("status").path("overall").path("level").asText(null);
+ if (status == null) {
+ throw new IllegalStateException("Kibana status response missing 'status.overall.level' field: " + body);
+ }
+ return status;
+ }
+ }
+
+ @Test
+ void withReuseShouldReuseTheSameContainer() {
+ Assumptions
+ .assumeThat(TestcontainersConfiguration.getInstance().environmentSupportsReuse())
+ .as("testcontainers.reuse.enable must be true")
+ .isTrue();
+
+ final String kibanaImage = "docker.elastic.co/kibana/kibana:9.2.4";
+
+ // Testcontainers.exposeHostPorts + host.testcontainers.internal lets Kibana reach ES
+ // from inside the container on any platform (Linux Docker Engine included).
+ // No withNetwork() on Kibana keeps the hash fully deterministic:
+ // - no dynamic network ID
+ // - the random tc-* alias added by GenericContainer's constructor is only serialised
+ // into the CreateContainerCmd when withNetwork() has been called, so it is absent here
+ // The host.testcontainers.internal extra-host IP is the same for kibana1 and kibana2
+ // because they start in the same JVM (same PortForwardingContainer instance).
+ // The first Kibana container must stay running while the second one starts, because
+ // withReuse(true) only skips JVM-shutdown cleanup — an explicit stop() still removes the
+ // container, so there would be nothing to find.
+ try (
+ ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE)
+ .withEnv("xpack.security.enabled", "false")
+ .withEnv("xpack.security.http.ssl.enabled", "false")
+ ) {
+ es.start();
+ int esMappedPort = es.getMappedPort(9200);
+ Testcontainers.exposeHostPorts(esMappedPort);
+ String esUrl = "http://" + GenericContainer.INTERNAL_HOST_HOSTNAME + ":" + esMappedPort;
+
+ KibanaContainer kibana1 = new KibanaContainer(kibanaImage).withElasticsearchUrl(esUrl).withReuse(true);
+ KibanaContainer kibana2 = new KibanaContainer(kibanaImage).withElasticsearchUrl(esUrl).withReuse(true);
+
+ try {
+ kibana1.start();
+ // kibana2 is started while kibana1 is still running; the reuse mechanism should
+ // find kibana1's container by hash and return the same container ID.
+ kibana2.start();
+
+ Assertions
+ .assertThat(kibana2.getContainerId())
+ .as("KibanaContainer with withReuse(true) should reuse the same container on subsequent starts")
+ .isEqualTo(kibana1.getContainerId());
+ } finally {
+ kibana1.stop();
+ kibana2.stop();
+ }
+ }
+ }
+
+ private static void applyTls(ElasticsearchContainer c, byte[] caCrt, byte[] nodeCrt, byte[] nodeKey) {
+ final String certDir = "/usr/share/elasticsearch/config/certs";
+
+ // Copy provided materials
+ c.withCopyToContainer(Transferable.of(caCrt, 0644), certDir + "/http_ca.crt");
+ c.withCopyToContainer(Transferable.of(nodeCrt, 0644), certDir + "/http.crt");
+ c.withCopyToContainer(Transferable.of(nodeKey, 0644), certDir + "/http.key");
+
+ // Disable ES bootstrap TLS autoconfiguration
+ c.withEnv("xpack.security.autoconfiguration.enabled", "false");
+ c.withEnv("xpack.security.enabled", "true");
+
+ // Configure ONLY HTTP TLS using exactly the provided files
+ c.withEnv("xpack.security.http.ssl.enabled", "true");
+ c.withEnv("xpack.security.http.ssl.certificate_authorities", "certs/http_ca.crt");
+ c.withEnv("xpack.security.http.ssl.certificate", "certs/http.crt");
+ c.withEnv("xpack.security.http.ssl.key", "certs/http.key");
+ }
+}
diff --git a/modules/elasticsearch/src/test/resources/logback-test.xml b/modules/elasticsearch/src/test/resources/logback-test.xml
index 535e406fc13..83ef7a1a3ef 100644
--- a/modules/elasticsearch/src/test/resources/logback-test.xml
+++ b/modules/elasticsearch/src/test/resources/logback-test.xml
@@ -12,5 +12,5 @@
-
+
diff --git a/modules/gcloud/build.gradle b/modules/gcloud/build.gradle
index c425fd70762..3a020bdb759 100644
--- a/modules/gcloud/build.gradle
+++ b/modules/gcloud/build.gradle
@@ -3,10 +3,11 @@ description = "Testcontainers :: GCloud"
dependencies {
api project(':testcontainers')
- testImplementation 'com.google.cloud:google-cloud-datastore:2.12.3'
- testImplementation 'com.google.cloud:google-cloud-firestore:3.7.0'
- testImplementation 'com.google.cloud:google-cloud-pubsub:1.120.24'
- testImplementation 'com.google.cloud:google-cloud-spanner:6.32.0'
- testImplementation 'com.google.cloud:google-cloud-bigtable:2.15.0'
- testImplementation 'org.assertj:assertj-core:3.23.1'
+ testImplementation platform("com.google.cloud:libraries-bom:26.84.0")
+ testImplementation 'com.google.cloud:google-cloud-bigquery'
+ testImplementation 'com.google.cloud:google-cloud-datastore'
+ testImplementation 'com.google.cloud:google-cloud-firestore'
+ testImplementation 'com.google.cloud:google-cloud-pubsub'
+ testImplementation 'com.google.cloud:google-cloud-spanner'
+ testImplementation 'com.google.cloud:google-cloud-bigtable'
}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/BigQueryEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/BigQueryEmulatorContainer.java
new file mode 100644
index 00000000000..6590c6cab5b
--- /dev/null
+++ b/modules/gcloud/src/main/java/org/testcontainers/containers/BigQueryEmulatorContainer.java
@@ -0,0 +1,46 @@
+package org.testcontainers.containers;
+
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Testcontainers implementation for BigQuery.
+ *
+ * Supported image: {@code ghcr.io/goccy/bigquery-emulator}
+ *
+ *
+ * @deprecated use {@link org.testcontainers.gcloud.BigQueryEmulatorContainer} instead.
+ */
+@Deprecated
+public class BigQueryEmulatorContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("ghcr.io/goccy/bigquery-emulator");
+
+ private static final int HTTP_PORT = 9050;
+
+ private static final int GRPC_PORT = 9060;
+
+ private static final String PROJECT_ID = "test-project";
+
+ public BigQueryEmulatorContainer(String image) {
+ this(DockerImageName.parse(image));
+ }
+
+ public BigQueryEmulatorContainer(DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ addExposedPorts(HTTP_PORT, GRPC_PORT);
+ withCommand("--project", PROJECT_ID);
+ }
+
+ public String getEmulatorHttpEndpoint() {
+ return String.format("http://%s:%d", getHost(), getMappedPort(HTTP_PORT));
+ }
+
+ public Integer getEmulatorGrpcPort() {
+ return getMappedPort(GRPC_PORT);
+ }
+
+ public String getProjectId() {
+ return PROJECT_ID;
+ }
+}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/BigtableEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/BigtableEmulatorContainer.java
index 84515fc7244..288cd499e37 100644
--- a/modules/gcloud/src/main/java/org/testcontainers/containers/BigtableEmulatorContainer.java
+++ b/modules/gcloud/src/main/java/org/testcontainers/containers/BigtableEmulatorContainer.java
@@ -1,19 +1,25 @@
package org.testcontainers.containers;
-import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;
/**
* A Bigtable container that relies in google cloud sdk.
- *
+ *
+ * Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk}
+ *
* Default port is 9000.
*
- * @author Eddú Meléndez
- * @author Ray Tsang
+ * @deprecated use {@link org.testcontainers.gcloud.BigtableEmulatorContainer} instead.
*/
+@Deprecated
public class BigtableEmulatorContainer extends GenericContainer {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(
+ "gcr.io/google.com/cloudsdktool/google-cloud-cli"
+ );
+
+ private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse(
"gcr.io/google.com/cloudsdktool/cloud-sdk"
);
@@ -21,12 +27,16 @@ public class BigtableEmulatorContainer extends GenericContainer
+ * Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk}
+ *
* Default port is 8081.
*
- * @author Eddú Meléndez
+ * @deprecated use {@link org.testcontainers.gcloud.DatastoreEmulatorContainer} instead.
*/
+@Deprecated
public class DatastoreEmulatorContainer extends GenericContainer {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(
+ "gcr.io/google.com/cloudsdktool/google-cloud-cli"
+ );
+
+ private static final DockerImageName CLOUD_SDK_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";
+ private static final String PROJECT_ID = "test-project";
+
+ private static final String CMD = String.format(
+ "gcloud beta emulators datastore start --project %s --host-port 0.0.0.0:8081",
+ PROJECT_ID
+ );
private static final int HTTP_PORT = 8081;
@@ -29,7 +40,7 @@ public DatastoreEmulatorContainer(final String image) {
public DatastoreEmulatorContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
- dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME);
withExposedPorts(HTTP_PORT);
setWaitStrategy(Wait.forHttp("/").forStatusCode(200));
@@ -57,4 +68,8 @@ public DatastoreEmulatorContainer withFlags(String flags) {
public String getEmulatorEndpoint() {
return getHost() + ":" + getMappedPort(HTTP_PORT);
}
+
+ public String getProjectId() {
+ return PROJECT_ID;
+ }
}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java
index a04c1e5831f..3c8d12f82ad 100644
--- a/modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java
+++ b/modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java
@@ -1,18 +1,25 @@
package org.testcontainers.containers;
-import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;
/**
* A Firestore container that relies in google cloud sdk.
- *
+ *
+ * Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk}
+ *
* Default port is 8080.
*
- * @author Eddú Meléndez
+ * @deprecated use {@link org.testcontainers.gcloud.FirestoreEmulatorContainer} instead.
*/
+@Deprecated
public class FirestoreEmulatorContainer extends GenericContainer {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(
+ "gcr.io/google.com/cloudsdktool/google-cloud-cli"
+ );
+
+ private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse(
"gcr.io/google.com/cloudsdktool/cloud-sdk"
);
@@ -20,13 +27,32 @@ public class FirestoreEmulatorContainer extends GenericContainer
+ * Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk}
+ *
* Default port is 8085.
*
- * @author Eddú Meléndez
+ * @deprecated use {@link org.testcontainers.gcloud.PubSubEmulatorContainer} instead.
*/
+@Deprecated
public class PubSubEmulatorContainer extends GenericContainer {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(
+ "gcr.io/google.com/cloudsdktool/google-cloud-cli"
+ );
+
+ private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse(
"gcr.io/google.com/cloudsdktool/cloud-sdk"
);
@@ -20,12 +27,16 @@ public class PubSubEmulatorContainer extends GenericContainer
+ * Supported image: {@code gcr.io/cloud-spanner-emulator/emulator}
*
- * @author Eddú Meléndez
+ * @deprecated use {@link org.testcontainers.gcloud.SpannerEmulatorContainer} instead.
*/
+@Deprecated
public class SpannerEmulatorContainer extends GenericContainer {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(
@@ -18,12 +21,16 @@ public class SpannerEmulatorContainer extends GenericContainer
+ * Supported image: {@code ghcr.io/goccy/bigquery-emulator}
+ *
+ */
+public class BigQueryEmulatorContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("ghcr.io/goccy/bigquery-emulator");
+
+ private static final int HTTP_PORT = 9050;
+
+ private static final int GRPC_PORT = 9060;
+
+ private static final String PROJECT_ID = "test-project";
+
+ public BigQueryEmulatorContainer(String image) {
+ this(DockerImageName.parse(image));
+ }
+
+ public BigQueryEmulatorContainer(DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ addExposedPorts(HTTP_PORT, GRPC_PORT);
+ withCommand("--project", PROJECT_ID);
+ }
+
+ public String getEmulatorHttpEndpoint() {
+ return String.format("http://%s:%d", getHost(), getMappedPort(HTTP_PORT));
+ }
+
+ public Integer getEmulatorGrpcPort() {
+ return getMappedPort(GRPC_PORT);
+ }
+
+ public String getProjectId() {
+ return PROJECT_ID;
+ }
+}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/gcloud/BigtableEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/gcloud/BigtableEmulatorContainer.java
new file mode 100644
index 00000000000..b1e62ae0aa1
--- /dev/null
+++ b/modules/gcloud/src/main/java/org/testcontainers/gcloud/BigtableEmulatorContainer.java
@@ -0,0 +1,53 @@
+package org.testcontainers.gcloud;
+
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A Bigtable container that relies in google cloud sdk.
+ *
+ * Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk}
+ *
+ * Default port is 9000.
+ */
+public class BigtableEmulatorContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(
+ "gcr.io/google.com/cloudsdktool/google-cloud-cli"
+ );
+
+ private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse(
+ "gcr.io/google.com/cloudsdktool/cloud-sdk"
+ );
+
+ private static final String CMD = "gcloud beta emulators bigtable start --host-port 0.0.0.0:9000";
+
+ private static final int PORT = 9000;
+
+ public BigtableEmulatorContainer(String image) {
+ this(DockerImageName.parse(image));
+ }
+
+ public BigtableEmulatorContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME);
+
+ withExposedPorts(PORT);
+ setWaitStrategy(Wait.forLogMessage(".*running.*$", 1));
+ withCommand("/bin/sh", "-c", CMD);
+ }
+
+ /**
+ * @return a host:port pair corresponding to the address on which the emulator is
+ * reachable from the test host machine. Directly usable as a parameter to the
+ * com.google.cloud.ServiceOptions.Builder#setHost(java.lang.String) method.
+ */
+ public String getEmulatorEndpoint() {
+ return getHost() + ":" + getEmulatorPort();
+ }
+
+ public int getEmulatorPort() {
+ return getMappedPort(PORT);
+ }
+}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/gcloud/DatastoreEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/gcloud/DatastoreEmulatorContainer.java
new file mode 100644
index 00000000000..360aedce686
--- /dev/null
+++ b/modules/gcloud/src/main/java/org/testcontainers/gcloud/DatastoreEmulatorContainer.java
@@ -0,0 +1,73 @@
+package org.testcontainers.gcloud;
+
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A Datastore container that relies in google cloud sdk.
+ *
+ * Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk}
+ *
+ * Default port is 8081.
+ */
+public class DatastoreEmulatorContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(
+ "gcr.io/google.com/cloudsdktool/google-cloud-cli"
+ );
+
+ private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse(
+ "gcr.io/google.com/cloudsdktool/cloud-sdk"
+ );
+
+ private static final String PROJECT_ID = "test-project";
+
+ private static final String CMD = String.format(
+ "gcloud beta emulators datastore start --project %s --host-port 0.0.0.0:8081",
+ PROJECT_ID
+ );
+
+ private static final int HTTP_PORT = 8081;
+
+ private String flags;
+
+ public DatastoreEmulatorContainer(final String image) {
+ this(DockerImageName.parse(image));
+ }
+
+ public DatastoreEmulatorContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME);
+
+ withExposedPorts(HTTP_PORT);
+ setWaitStrategy(Wait.forHttp("/").forStatusCode(200));
+ }
+
+ @Override
+ protected void configure() {
+ String command = CMD;
+ if (this.flags != null && !this.flags.isEmpty()) {
+ command += " " + this.flags;
+ }
+ withCommand("/bin/sh", "-c", command);
+ }
+
+ public DatastoreEmulatorContainer withFlags(String flags) {
+ this.flags = flags;
+ return this;
+ }
+
+ /**
+ * @return a host:port pair corresponding to the address on which the emulator is
+ * reachable from the test host machine. Directly usable as a parameter to the
+ * com.google.cloud.ServiceOptions.Builder#setHost(java.lang.String) method.
+ */
+ public String getEmulatorEndpoint() {
+ return getHost() + ":" + getMappedPort(HTTP_PORT);
+ }
+
+ public String getProjectId() {
+ return PROJECT_ID;
+ }
+}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/gcloud/FirestoreEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/gcloud/FirestoreEmulatorContainer.java
new file mode 100644
index 00000000000..140f7166307
--- /dev/null
+++ b/modules/gcloud/src/main/java/org/testcontainers/gcloud/FirestoreEmulatorContainer.java
@@ -0,0 +1,64 @@
+package org.testcontainers.gcloud;
+
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A Firestore container that relies in google cloud sdk.
+ *
+ * Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk}
+ *
+ * Default port is 8080.
+ */
+public class FirestoreEmulatorContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(
+ "gcr.io/google.com/cloudsdktool/google-cloud-cli"
+ );
+
+ private static final DockerImageName CLOUD_SDK_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";
+
+ private static final int PORT = 8080;
+
+ private String flags;
+
+ public FirestoreEmulatorContainer(String image) {
+ this(DockerImageName.parse(image));
+ }
+
+ public FirestoreEmulatorContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME);
+
+ withExposedPorts(PORT);
+ setWaitStrategy(Wait.forLogMessage(".*running.*$", 1));
+ }
+
+ @Override
+ protected void configure() {
+ String command = CMD;
+ if (this.flags != null && !this.flags.isEmpty()) {
+ command += " " + this.flags;
+ }
+ withCommand("/bin/sh", "-c", command);
+ }
+
+ public FirestoreEmulatorContainer withFlags(String flags) {
+ this.flags = flags;
+ return this;
+ }
+
+ /**
+ * @return a host:port pair corresponding to the address on which the emulator is
+ * reachable from the test host machine. Directly usable as a parameter to the
+ * com.google.cloud.ServiceOptions.Builder#setHost(java.lang.String) method.
+ */
+ public String getEmulatorEndpoint() {
+ return getHost() + ":" + getMappedPort(8080);
+ }
+}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/gcloud/PubSubEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/gcloud/PubSubEmulatorContainer.java
new file mode 100644
index 00000000000..417ee8dc524
--- /dev/null
+++ b/modules/gcloud/src/main/java/org/testcontainers/gcloud/PubSubEmulatorContainer.java
@@ -0,0 +1,49 @@
+package org.testcontainers.gcloud;
+
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A PubSub container that relies in google cloud sdk.
+ *
+ * Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk}
+ *
+ * Default port is 8085.
+ */
+public class PubSubEmulatorContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(
+ "gcr.io/google.com/cloudsdktool/google-cloud-cli"
+ );
+
+ private static final DockerImageName CLOUD_SDK_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";
+
+ private static final int PORT = 8085;
+
+ public PubSubEmulatorContainer(String image) {
+ this(DockerImageName.parse(image));
+ }
+
+ public PubSubEmulatorContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME);
+
+ withExposedPorts(8085);
+ setWaitStrategy(Wait.forLogMessage(".*started.*$", 1));
+ withCommand("/bin/sh", "-c", CMD);
+ }
+
+ /**
+ * @return a host:port pair corresponding to the address on which the emulator is
+ * reachable from the test host machine. Directly usable as a parameter to the
+ * io.grpc.ManagedChannelBuilder#forTarget(java.lang.String) method.
+ */
+ public String getEmulatorEndpoint() {
+ return getHost() + ":" + getMappedPort(PORT);
+ }
+}
diff --git a/modules/gcloud/src/main/java/org/testcontainers/gcloud/SpannerEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/gcloud/SpannerEmulatorContainer.java
new file mode 100644
index 00000000000..aa99f6be5f4
--- /dev/null
+++ b/modules/gcloud/src/main/java/org/testcontainers/gcloud/SpannerEmulatorContainer.java
@@ -0,0 +1,50 @@
+package org.testcontainers.gcloud;
+
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A Spanner container. Default ports: 9010 for GRPC and 9020 for HTTP.
+ *
+ * Supported image: {@code gcr.io/cloud-spanner-emulator/emulator}
+ */
+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(String image) {
+ this(DockerImageName.parse(image));
+ }
+
+ public SpannerEmulatorContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+
+ withExposedPorts(GRPC_PORT, HTTP_PORT);
+ setWaitStrategy(Wait.forLogMessage(".*Cloud Spanner emulator running\\..*", 1));
+ }
+
+ /**
+ * @return a host:port pair corresponding to the address on which the emulator's
+ * gRPC endpoint is reachable from the test host machine. Directly usable as a parameter to the
+ * com.google.cloud.spanner.SpannerOptions.Builder#setEmulatorHost(java.lang.String) method.
+ */
+ public String getEmulatorGrpcEndpoint() {
+ return getHost() + ":" + getMappedPort(GRPC_PORT);
+ }
+
+ /**
+ * @return a host:port pair corresponding to the address on which the emulator's
+ * HTTP REST endpoint is reachable from the test host machine.
+ */
+ public String getEmulatorHttpEndpoint() {
+ return getHost() + ":" + getMappedPort(HTTP_PORT);
+ }
+}
diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java
deleted file mode 100644
index 3e22231220b..00000000000
--- a/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package org.testcontainers.containers;
-
-import com.google.api.gax.core.CredentialsProvider;
-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.bigtable.admin.v2.BigtableTableAdminClient;
-import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest;
-import com.google.cloud.bigtable.admin.v2.models.Table;
-import com.google.cloud.bigtable.admin.v2.stub.BigtableTableAdminStubSettings;
-import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub;
-import com.google.cloud.bigtable.data.v2.BigtableDataClient;
-import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
-import com.google.cloud.bigtable.data.v2.models.Row;
-import com.google.cloud.bigtable.data.v2.models.RowCell;
-import com.google.cloud.bigtable.data.v2.models.RowMutation;
-import io.grpc.ManagedChannel;
-import io.grpc.ManagedChannelBuilder;
-import org.junit.Rule;
-import org.junit.Test;
-import org.testcontainers.utility.DockerImageName;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class BigtableEmulatorContainerTest {
-
- public static final String PROJECT_ID = "test-project";
-
- public static final String INSTANCE_ID = "test-instance";
-
- @Rule
- // emulatorContainer {
- public BigtableEmulatorContainer emulator = new BigtableEmulatorContainer(
- DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")
- );
-
- // }
-
- @Test
- // testWithEmulatorContainer {
- public void testSimple() throws IOException, InterruptedException, ExecutionException {
- ManagedChannel channel = ManagedChannelBuilder.forTarget(emulator.getEmulatorEndpoint()).usePlaintext().build();
-
- TransportChannelProvider channelProvider = FixedTransportChannelProvider.create(
- GrpcTransportChannel.create(channel)
- );
- NoCredentialsProvider credentialsProvider = NoCredentialsProvider.create();
-
- try {
- createTable(channelProvider, credentialsProvider, "test-table");
-
- BigtableDataClient client = BigtableDataClient.create(
- BigtableDataSettings
- .newBuilderForEmulator(emulator.getHost(), emulator.getEmulatorPort())
- .setProjectId(PROJECT_ID)
- .setInstanceId(INSTANCE_ID)
- .build()
- );
-
- client.mutateRow(RowMutation.create("test-table", "1").setCell("name", "firstName", "Ray"));
-
- Row row = client.readRow("test-table", "1");
- List cells = row.getCells("name", "firstName");
-
- assertThat(cells).isNotNull().hasSize(1);
- assertThat(cells.get(0).getValue().toStringUtf8()).isEqualTo("Ray");
- } finally {
- channel.shutdown();
- }
- }
-
- // }
-
- // createTable {
- private void createTable(
- TransportChannelProvider channelProvider,
- CredentialsProvider credentialsProvider,
- String tableName
- ) throws IOException {
- EnhancedBigtableTableAdminStub stub = EnhancedBigtableTableAdminStub.createEnhanced(
- BigtableTableAdminStubSettings
- .newBuilder()
- .setTransportChannelProvider(channelProvider)
- .setCredentialsProvider(credentialsProvider)
- .build()
- );
-
- try (BigtableTableAdminClient client = BigtableTableAdminClient.create(PROJECT_ID, INSTANCE_ID, stub)) {
- Table table = client.createTable(CreateTableRequest.of(tableName).addFamily("name"));
- }
- }
- // }
-}
diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/FirestoreEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/containers/FirestoreEmulatorContainerTest.java
deleted file mode 100644
index c87f8e5176a..00000000000
--- a/modules/gcloud/src/test/java/org/testcontainers/containers/FirestoreEmulatorContainerTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package org.testcontainers.containers;
-
-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 java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class FirestoreEmulatorContainerTest {
-
- @Rule
- // emulatorContainer {
- public FirestoreEmulatorContainer emulator = new FirestoreEmulatorContainer(
- DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")
- );
-
- // }
-
- // testWithEmulatorContainer {
- @Test
- public void testSimple() throws ExecutionException, InterruptedException {
- FirestoreOptions options = FirestoreOptions
- .getDefaultInstance()
- .toBuilder()
- .setHost(emulator.getEmulatorEndpoint())
- .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/gcloud/BigQueryEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/BigQueryEmulatorContainerTest.java
new file mode 100644
index 00000000000..7d657830fa4
--- /dev/null
+++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/BigQueryEmulatorContainerTest.java
@@ -0,0 +1,204 @@
+package org.testcontainers.gcloud;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.gax.core.NoCredentialsProvider;
+import com.google.api.gax.grpc.GrpcTransportChannel;
+import com.google.api.gax.rpc.FixedTransportChannelProvider;
+import com.google.cloud.NoCredentials;
+import com.google.cloud.bigquery.BigQuery;
+import com.google.cloud.bigquery.BigQueryOptions;
+import com.google.cloud.bigquery.DatasetId;
+import com.google.cloud.bigquery.DatasetInfo;
+import com.google.cloud.bigquery.Field;
+import com.google.cloud.bigquery.QueryJobConfiguration;
+import com.google.cloud.bigquery.Schema;
+import com.google.cloud.bigquery.StandardSQLTypeName;
+import com.google.cloud.bigquery.StandardTableDefinition;
+import com.google.cloud.bigquery.TableDefinition;
+import com.google.cloud.bigquery.TableId;
+import com.google.cloud.bigquery.TableInfo;
+import com.google.cloud.bigquery.TableResult;
+import com.google.cloud.bigquery.storage.v1.AppendRowsResponse;
+import com.google.cloud.bigquery.storage.v1.BatchCommitWriteStreamsRequest;
+import com.google.cloud.bigquery.storage.v1.BatchCommitWriteStreamsResponse;
+import com.google.cloud.bigquery.storage.v1.BigQueryWriteClient;
+import com.google.cloud.bigquery.storage.v1.BigQueryWriteSettings;
+import com.google.cloud.bigquery.storage.v1.CreateWriteStreamRequest;
+import com.google.cloud.bigquery.storage.v1.FinalizeWriteStreamRequest;
+import com.google.cloud.bigquery.storage.v1.FinalizeWriteStreamResponse;
+import com.google.cloud.bigquery.storage.v1.JsonStreamWriter;
+import com.google.cloud.bigquery.storage.v1.TableName;
+import com.google.cloud.bigquery.storage.v1.WriteStream;
+import io.grpc.ManagedChannelBuilder;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.jupiter.api.Test;
+import org.threeten.bp.Duration;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class BigQueryEmulatorContainerTest {
+
+ @Test
+ void testHttpEndpoint() throws Exception {
+ try (
+ // emulatorContainer {
+ BigQueryEmulatorContainer container = new BigQueryEmulatorContainer("ghcr.io/goccy/bigquery-emulator:0.4.3")
+ // }
+ ) {
+ container.start();
+
+ // bigQueryClient {
+ String url = container.getEmulatorHttpEndpoint();
+ BigQueryOptions options = BigQueryOptions
+ .newBuilder()
+ .setProjectId(container.getProjectId())
+ .setHost(url)
+ .setLocation(url)
+ .setCredentials(NoCredentials.getInstance())
+ .build();
+ BigQuery bigQuery = options.getService();
+ // }
+
+ String fn =
+ "CREATE FUNCTION testr(arr ARRAY>) AS ((SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem))";
+
+ bigQuery.query(QueryJobConfiguration.newBuilder(fn).build());
+
+ String sql =
+ "SELECT testr([STRUCT(\"foo\", 10), STRUCT(\"bar\", 40), STRUCT(\"foo\", 20)])";
+ TableResult result = bigQuery.query(QueryJobConfiguration.newBuilder(sql).build());
+ List values = result
+ .streamValues()
+ .map(fieldValues -> fieldValues.get(0).getNumericValue())
+ .collect(Collectors.toList());
+ assertThat(values).containsOnly(BigDecimal.valueOf(30));
+ }
+ }
+
+ @Test
+ void testGrcpEndpoint() throws Exception {
+ try (
+ BigQueryEmulatorContainer container = new BigQueryEmulatorContainer("ghcr.io/goccy/bigquery-emulator:0.6.5")
+ ) {
+ container.start();
+
+ BigQuery bigQuery = getBigQuery(container);
+ String tableName = "test-table";
+ String datasetName = "test-dataset";
+
+ bigQuery.create(DatasetInfo.of(DatasetId.of(container.getProjectId(), datasetName)));
+
+ Schema schema = Schema.of(Field.of("name", StandardSQLTypeName.STRING));
+
+ TableId tableId = TableId.of(datasetName, tableName);
+ TableDefinition tableDefinition = StandardTableDefinition.of(schema);
+ TableInfo tableInfo = TableInfo.newBuilder(tableId, tableDefinition).build();
+
+ bigQuery.create(tableInfo);
+
+ BigQueryWriteSettings.Builder bigQueryWriteSettingsBuilder = BigQueryWriteSettings.newBuilder();
+
+ bigQueryWriteSettingsBuilder
+ .createWriteStreamSettings()
+ .setRetrySettings(
+ bigQueryWriteSettingsBuilder
+ .createWriteStreamSettings()
+ .getRetrySettings()
+ .toBuilder()
+ .setTotalTimeout(Duration.ofSeconds(60))
+ .build()
+ );
+
+ BigQueryWriteClient bigQueryWriteClient = BigQueryWriteClient.create(
+ bigQueryWriteSettingsBuilder
+ .setTransportChannelProvider(
+ FixedTransportChannelProvider.create(
+ GrpcTransportChannel.create(
+ ManagedChannelBuilder
+ .forAddress(container.getHost(), container.getEmulatorGrpcPort())
+ .usePlaintext()
+ .build()
+ )
+ )
+ )
+ .setCredentialsProvider(NoCredentialsProvider.create())
+ .build()
+ );
+
+ TableName parentTable = TableName.of(container.getProjectId(), datasetName, tableName);
+ CreateWriteStreamRequest createWriteStreamRequest = CreateWriteStreamRequest
+ .newBuilder()
+ .setParent(parentTable.toString())
+ .setWriteStream(WriteStream.newBuilder().setType(WriteStream.Type.PENDING))
+ .build();
+
+ WriteStream writeStream = bigQueryWriteClient.createWriteStream(createWriteStreamRequest);
+
+ JsonStreamWriter writer = JsonStreamWriter
+ .newBuilder(writeStream.getName(), writeStream.getTableSchema(), bigQueryWriteClient)
+ .build();
+
+ JSONArray jsonArray = new JSONArray();
+ JSONObject record1 = new JSONObject();
+ record1.put("name", "Alice");
+ jsonArray.put(record1);
+
+ JSONObject record2 = new JSONObject();
+ record2.put("name", "Bob");
+ jsonArray.put(record2);
+
+ ApiFuture future = writer.append(jsonArray);
+ AppendRowsResponse response = future.get();
+
+ FinalizeWriteStreamRequest finalizeRequest = FinalizeWriteStreamRequest
+ .newBuilder()
+ .setName(writeStream.getName())
+ .build();
+ FinalizeWriteStreamResponse finalizeResponse = bigQueryWriteClient.finalizeWriteStream(finalizeRequest);
+
+ BatchCommitWriteStreamsRequest commitRequest = BatchCommitWriteStreamsRequest
+ .newBuilder()
+ .setParent(parentTable.toString())
+ .addWriteStreams(writeStream.getName())
+ .build();
+ BatchCommitWriteStreamsResponse commitResponse = bigQueryWriteClient.batchCommitWriteStreams(commitRequest);
+
+ writer.close();
+
+ String sql = String.format(
+ "SELECT name FROM `%s.%s.%s` ORDER BY name",
+ container.getProjectId(),
+ datasetName,
+ tableName
+ );
+ TableResult result = bigQuery.query(QueryJobConfiguration.newBuilder(sql).build());
+
+ List names = result
+ .streamValues()
+ .map(row -> row.get("name").getStringValue())
+ .collect(Collectors.toList());
+
+ assertThat(names).containsExactly("Alice", "Bob");
+
+ bigQueryWriteClient.shutdown();
+ bigQueryWriteClient.close();
+ }
+ }
+
+ private BigQuery getBigQuery(BigQueryEmulatorContainer container) {
+ String url = container.getEmulatorHttpEndpoint();
+ return BigQueryOptions
+ .newBuilder()
+ .setProjectId(container.getProjectId())
+ .setHost(url)
+ .setLocation(url)
+ .setCredentials(NoCredentials.getInstance())
+ .build()
+ .getService();
+ }
+}
diff --git a/modules/gcloud/src/test/java/org/testcontainers/gcloud/BigtableEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/BigtableEmulatorContainerTest.java
new file mode 100644
index 00000000000..43f25876326
--- /dev/null
+++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/BigtableEmulatorContainerTest.java
@@ -0,0 +1,102 @@
+package org.testcontainers.gcloud;
+
+import com.google.api.gax.core.CredentialsProvider;
+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.bigtable.admin.v2.BigtableTableAdminClient;
+import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest;
+import com.google.cloud.bigtable.admin.v2.models.Table;
+import com.google.cloud.bigtable.admin.v2.stub.BigtableTableAdminStubSettings;
+import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub;
+import com.google.cloud.bigtable.data.v2.BigtableDataClient;
+import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
+import com.google.cloud.bigtable.data.v2.internal.TableAdminRequestContext;
+import com.google.cloud.bigtable.data.v2.models.Row;
+import com.google.cloud.bigtable.data.v2.models.RowCell;
+import com.google.cloud.bigtable.data.v2.models.RowMutation;
+import com.google.cloud.bigtable.data.v2.models.TableId;
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class BigtableEmulatorContainerTest {
+
+ private static final String PROJECT_ID = "test-project";
+
+ private static final String INSTANCE_ID = "test-instance";
+
+ @Test
+ // testWithEmulatorContainer {
+ void testSimple() throws IOException {
+ try (
+ // emulatorContainer {
+ BigtableEmulatorContainer emulator = new BigtableEmulatorContainer(
+ DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:583.0.0-emulators")
+ );
+ // }
+ ) {
+ emulator.start();
+ ManagedChannel channel = ManagedChannelBuilder
+ .forTarget(emulator.getEmulatorEndpoint())
+ .usePlaintext()
+ .build();
+
+ TransportChannelProvider channelProvider = FixedTransportChannelProvider.create(
+ GrpcTransportChannel.create(channel)
+ );
+ NoCredentialsProvider credentialsProvider = NoCredentialsProvider.create();
+ createTable(channelProvider, credentialsProvider, "test-table");
+ try (
+ BigtableDataClient client = BigtableDataClient.create(
+ BigtableDataSettings
+ .newBuilderForEmulator(emulator.getHost(), emulator.getEmulatorPort())
+ .setProjectId(PROJECT_ID)
+ .setInstanceId(INSTANCE_ID)
+ .build()
+ )
+ ) {
+ client.mutateRow(RowMutation.create(TableId.of("test-table"), "1").setCell("name", "firstName", "Ray"));
+
+ Row row = client.readRow(TableId.of("test-table"), "1");
+ List cells = row.getCells("name", "firstName");
+
+ assertThat(cells).isNotNull().hasSize(1);
+ assertThat(cells.get(0).getValue().toStringUtf8()).isEqualTo("Ray");
+ } finally {
+ channel.shutdown();
+ }
+ }
+ }
+
+ // }
+
+ // createTable {
+ private void createTable(
+ TransportChannelProvider channelProvider,
+ CredentialsProvider credentialsProvider,
+ String tableName
+ ) throws IOException {
+ TableAdminRequestContext requestContext = TableAdminRequestContext.create(PROJECT_ID, INSTANCE_ID);
+ EnhancedBigtableTableAdminStub stub = EnhancedBigtableTableAdminStub.createEnhanced(
+ BigtableTableAdminStubSettings
+ .newBuilder()
+ .setTransportChannelProvider(channelProvider)
+ .setCredentialsProvider(credentialsProvider)
+ .build(),
+ requestContext
+ );
+
+ try (BigtableTableAdminClient client = BigtableTableAdminClient.create(PROJECT_ID, INSTANCE_ID, stub)) {
+ Table table = client.createTable(CreateTableRequest.of(tableName).addFamily("name"));
+ }
+ }
+ // }
+}
diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/DatastoreEmulatorContainerTest.java
similarity index 50%
rename from modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java
rename to modules/gcloud/src/test/java/org/testcontainers/gcloud/DatastoreEmulatorContainerTest.java
index b1413edefba..d3e7a01db5c 100644
--- a/modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java
+++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/DatastoreEmulatorContainerTest.java
@@ -1,4 +1,4 @@
-package org.testcontainers.containers;
+package org.testcontainers.gcloud;
import com.google.cloud.NoCredentials;
import com.google.cloud.ServiceOptions;
@@ -6,8 +6,7 @@
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.junit.jupiter.api.Test;
import org.testcontainers.utility.DockerImageName;
import java.io.IOException;
@@ -16,40 +15,41 @@
public class DatastoreEmulatorContainerTest {
- @Rule
- // creatingDatastoreEmulatorContainer {
- public DatastoreEmulatorContainer emulator = new DatastoreEmulatorContainer(
- DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")
- );
-
- // }
-
// startingDatastoreEmulatorContainer {
@Test
public void testSimple() {
- DatastoreOptions options = DatastoreOptions
- .newBuilder()
- .setHost(emulator.getEmulatorEndpoint())
- .setCredentials(NoCredentials.getInstance())
- .setRetrySettings(ServiceOptions.getNoRetrySettings())
- .setProjectId("test-project")
- .build();
- Datastore datastore = options.getService();
+ try (
+ // creatingDatastoreEmulatorContainer {
+ DatastoreEmulatorContainer emulator = new DatastoreEmulatorContainer(
+ DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:583.0.0-emulators")
+ );
+ // }
+ ) {
+ emulator.start();
+ DatastoreOptions options = DatastoreOptions
+ .newBuilder()
+ .setHost(emulator.getEmulatorEndpoint())
+ .setCredentials(NoCredentials.getInstance())
+ .setRetrySettings(ServiceOptions.getNoRetrySettings())
+ .setProjectId(emulator.getProjectId())
+ .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);
+ 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");
+ assertThat(datastore.get(key).getString("description")).isEqualTo("my description");
+ }
}
// }
@Test
- public void testWithFlags() throws IOException, InterruptedException {
+ void testWithFlags() throws IOException, InterruptedException {
try (
DatastoreEmulatorContainer emulator = new DatastoreEmulatorContainer(
- "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators"
+ "gcr.io/google.com/cloudsdktool/google-cloud-cli:583.0.0-emulators"
)
.withFlags("--consistency 1.0")
) {
@@ -61,10 +61,10 @@ public void testWithFlags() throws IOException, InterruptedException {
}
@Test
- public void testWithMultipleFlags() throws IOException, InterruptedException {
+ void testWithMultipleFlags() throws IOException, InterruptedException {
try (
DatastoreEmulatorContainer emulator = new DatastoreEmulatorContainer(
- "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators"
+ "gcr.io/google.com/cloudsdktool/google-cloud-cli:583.0.0-emulators"
)
.withFlags("--consistency 1.0 --data-dir /root/.config/test-gcloud")
) {
diff --git a/modules/gcloud/src/test/java/org/testcontainers/gcloud/FirestoreEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/FirestoreEmulatorContainerTest.java
new file mode 100644
index 00000000000..ca1da0c2892
--- /dev/null
+++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/FirestoreEmulatorContainerTest.java
@@ -0,0 +1,73 @@
+package org.testcontainers.gcloud;
+
+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.jupiter.api.Test;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class FirestoreEmulatorContainerTest {
+
+ // testWithEmulatorContainer {
+ @Test
+ void testSimple() throws ExecutionException, InterruptedException {
+ try (
+ // emulatorContainer {
+ FirestoreEmulatorContainer emulator = new FirestoreEmulatorContainer(
+ DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:583.0.0-emulators")
+ );
+ // }
+ ) {
+ emulator.start();
+ FirestoreOptions options = FirestoreOptions
+ .getDefaultInstance()
+ .toBuilder()
+ .setHost(emulator.getEmulatorEndpoint())
+ .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");
+ }
+ }
+
+ // }
+
+ @Test
+ void testWithFlags() {
+ try (
+ FirestoreEmulatorContainer emulator = new FirestoreEmulatorContainer(
+ "gcr.io/google.com/cloudsdktool/google-cloud-cli:583.0.0-emulators"
+ )
+ .withFlags("--database-mode datastore-mode")
+ ) {
+ emulator.start();
+
+ assertThat(emulator.getContainerInfo().getConfig().getCmd())
+ .anyMatch(e -> e.contains("--database-mode datastore-mode"));
+ }
+ }
+}
diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/PubSubEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java
similarity index 52%
rename from modules/gcloud/src/test/java/org/testcontainers/containers/PubSubEmulatorContainerTest.java
rename to modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java
index aeb92bc4a69..54d557d9de9 100644
--- a/modules/gcloud/src/test/java/org/testcontainers/containers/PubSubEmulatorContainerTest.java
+++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java
@@ -1,4 +1,4 @@
-package org.testcontainers.containers;
+package org.testcontainers.gcloud;
import com.google.api.gax.core.NoCredentialsProvider;
import com.google.api.gax.grpc.GrpcTransportChannel;
@@ -22,70 +22,73 @@
import com.google.pubsub.v1.TopicName;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.testcontainers.utility.DockerImageName;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
-public class PubSubEmulatorContainerTest {
+class PubSubEmulatorContainerTest {
- public static final String PROJECT_ID = "my-project-id";
-
- @Rule
- // emulatorContainer {
- public PubSubEmulatorContainer emulator = new PubSubEmulatorContainer(
- DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")
- );
-
- // }
+ private static final String PROJECT_ID = "my-project-id";
@Test
// testWithEmulatorContainer {
- public void testSimple() throws IOException {
- String hostport = emulator.getEmulatorEndpoint();
- ManagedChannel channel = ManagedChannelBuilder.forTarget(hostport).usePlaintext().build();
- try {
- TransportChannelProvider channelProvider = FixedTransportChannelProvider.create(
- GrpcTransportChannel.create(channel)
+ void testSimple() throws IOException {
+ try (
+ // emulatorContainer {
+ PubSubEmulatorContainer emulator = new PubSubEmulatorContainer(
+ DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:583.0.0-emulators")
);
- 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
+ // }
+ ) {
+ emulator.start();
+ String hostport = emulator.getEmulatorEndpoint();
+ 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()
- .setMaxMessages(1)
- .setSubscription(ProjectSubscriptionName.format(PROJECT_ID, subscriptionId))
+ .setData(ByteString.copyFromUtf8("test message"))
.build();
- PullResponse pullResponse = subscriber.pullCallable().call(pullRequest);
+ publisher.publish(message);
- assertThat(pullResponse.getReceivedMessagesList()).hasSize(1);
- assertThat(pullResponse.getReceivedMessages(0).getMessage().getData().toStringUtf8())
- .isEqualTo("test 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();
}
- } finally {
- channel.shutdown();
}
}
diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/SpannerEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java
similarity index 56%
rename from modules/gcloud/src/test/java/org/testcontainers/containers/SpannerEmulatorContainerTest.java
rename to modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java
index b246e7e1f0d..2d6ac963882 100644
--- a/modules/gcloud/src/test/java/org/testcontainers/containers/SpannerEmulatorContainerTest.java
+++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java
@@ -1,4 +1,4 @@
-package org.testcontainers.containers;
+package org.testcontainers.gcloud;
import com.google.cloud.NoCredentials;
import com.google.cloud.spanner.Database;
@@ -14,8 +14,7 @@
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.junit.jupiter.api.Test;
import org.testcontainers.utility.DockerImageName;
import java.util.Arrays;
@@ -23,15 +22,7 @@
import static org.assertj.core.api.Assertions.assertThat;
-public class SpannerEmulatorContainerTest {
-
- @Rule
- // emulatorContainer {
- public SpannerEmulatorContainer emulator = new SpannerEmulatorContainer(
- DockerImageName.parse("gcr.io/cloud-spanner-emulator/emulator:1.4.0")
- );
-
- // }
+class SpannerEmulatorContainerTest {
private static final String PROJECT_NAME = "test-project";
@@ -41,38 +32,47 @@ public class SpannerEmulatorContainerTest {
// testWithEmulatorContainer {
@Test
- public void testSimple() throws ExecutionException, InterruptedException {
- SpannerOptions options = SpannerOptions
- .newBuilder()
- .setEmulatorHost(emulator.getEmulatorGrpcEndpoint())
- .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");
+ void testSimple() throws ExecutionException, InterruptedException {
+ try (
+ // emulatorContainer {
+ SpannerEmulatorContainer emulator = new SpannerEmulatorContainer(
+ DockerImageName.parse("gcr.io/cloud-spanner-emulator/emulator:1.4.0")
+ );
+ // }
+ ) {
+ emulator.start();
+ SpannerOptions options = SpannerOptions
+ .newBuilder()
+ .setEmulatorHost(emulator.getEmulatorGrpcEndpoint())
+ .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");
+ }
}
// }
diff --git a/modules/gcloud/src/test/resources/logback-test.xml b/modules/gcloud/src/test/resources/logback-test.xml
index 535e406fc13..83ef7a1a3ef 100644
--- a/modules/gcloud/src/test/resources/logback-test.xml
+++ b/modules/gcloud/src/test/resources/logback-test.xml
@@ -12,5 +12,5 @@
-
+
diff --git a/modules/grafana/build.gradle b/modules/grafana/build.gradle
new file mode 100644
index 00000000000..93bae9750b4
--- /dev/null
+++ b/modules/grafana/build.gradle
@@ -0,0 +1,14 @@
+description = "Testcontainers :: Grafana"
+
+dependencies {
+ api project(':testcontainers')
+
+ testImplementation 'io.rest-assured:rest-assured:5.5.7'
+ testImplementation 'io.micrometer:micrometer-registry-otlp:1.17.0'
+ testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.1.8'
+
+ testImplementation platform('io.opentelemetry:opentelemetry-bom:1.65.0')
+ testImplementation 'io.opentelemetry:opentelemetry-api'
+ testImplementation 'io.opentelemetry:opentelemetry-sdk'
+ testImplementation 'io.opentelemetry:opentelemetry-exporter-otlp'
+}
diff --git a/modules/grafana/src/main/java/org/testcontainers/grafana/LgtmStackContainer.java b/modules/grafana/src/main/java/org/testcontainers/grafana/LgtmStackContainer.java
new file mode 100644
index 00000000000..52443d7ba8a
--- /dev/null
+++ b/modules/grafana/src/main/java/org/testcontainers/grafana/LgtmStackContainer.java
@@ -0,0 +1,81 @@
+package org.testcontainers.grafana;
+
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Testcontainers implementation for Grafana OTel LGTM.
+ *
+ * Supported image: {@code grafana/otel-lgtm}
+ *
+ * Exposed ports:
+ *
+ * Grafana: 3000
+ * Tempo: 3200
+ * OTel Http: 4317
+ * OTel Grpc: 4318
+ * Prometheus: 9090
+ *
+ */
+@Slf4j
+public class LgtmStackContainer extends GenericContainer {
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("grafana/otel-lgtm");
+
+ private static final int GRAFANA_PORT = 3000;
+
+ private static final int OTLP_GRPC_PORT = 4317;
+
+ private static final int OTLP_HTTP_PORT = 4318;
+
+ private static final int LOKI_PORT = 3100;
+
+ private static final int TEMPO_PORT = 3200;
+
+ private static final int PROMETHEUS_PORT = 9090;
+
+ public LgtmStackContainer(String image) {
+ this(DockerImageName.parse(image));
+ }
+
+ public LgtmStackContainer(DockerImageName image) {
+ super(image);
+ image.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ withExposedPorts(GRAFANA_PORT, TEMPO_PORT, LOKI_PORT, OTLP_GRPC_PORT, OTLP_HTTP_PORT, PROMETHEUS_PORT);
+ waitingFor(
+ Wait.forLogMessage(".*The OpenTelemetry collector and the Grafana LGTM stack are up and running.*\\s", 1)
+ );
+ }
+
+ @Override
+ protected void containerIsStarted(InspectContainerResponse containerInfo) {
+ log.info("Access to the Grafana dashboard: {}", getGrafanaHttpUrl());
+ }
+
+ public String getOtlpGrpcUrl() {
+ return "http://" + getHost() + ":" + getMappedPort(OTLP_GRPC_PORT);
+ }
+
+ public String getTempoUrl() {
+ return "http://" + getHost() + ":" + getMappedPort(TEMPO_PORT);
+ }
+
+ public String getLokiUrl() {
+ return "http://" + getHost() + ":" + getMappedPort(LOKI_PORT);
+ }
+
+ public String getOtlpHttpUrl() {
+ return "http://" + getHost() + ":" + getMappedPort(OTLP_HTTP_PORT);
+ }
+
+ public String getPrometheusHttpUrl() {
+ return "http://" + getHost() + ":" + getMappedPort(PROMETHEUS_PORT);
+ }
+
+ public String getGrafanaHttpUrl() {
+ return "http://" + getHost() + ":" + getMappedPort(GRAFANA_PORT);
+ }
+}
diff --git a/modules/grafana/src/test/java/org/testcontainers/grafana/LgtmStackContainerTest.java b/modules/grafana/src/test/java/org/testcontainers/grafana/LgtmStackContainerTest.java
new file mode 100644
index 00000000000..ff8d5cb6609
--- /dev/null
+++ b/modules/grafana/src/test/java/org/testcontainers/grafana/LgtmStackContainerTest.java
@@ -0,0 +1,156 @@
+package org.testcontainers.grafana;
+
+import io.micrometer.core.instrument.Clock;
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.registry.otlp.OtlpConfig;
+import io.micrometer.registry.otlp.OtlpMeterRegistry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.logs.Logger;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter;
+import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.logs.SdkLoggerProvider;
+import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
+import io.restassured.RestAssured;
+import io.restassured.response.Response;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+import uk.org.webcompere.systemstubs.SystemStubs;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class LgtmStackContainerTest {
+
+ @Test
+ void shouldPublishMetricsTracesAndLogs() throws Exception {
+ try ( // container {
+ LgtmStackContainer lgtm = new LgtmStackContainer("grafana/otel-lgtm:0.11.1")
+ // }
+ ) {
+ lgtm.start();
+
+ OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter
+ .builder()
+ .setTimeout(Duration.ofSeconds(1))
+ .setEndpoint(lgtm.getOtlpGrpcUrl())
+ .build();
+
+ OtlpGrpcLogRecordExporter logExporter = OtlpGrpcLogRecordExporter
+ .builder()
+ .setTimeout(Duration.ofSeconds(1))
+ .setEndpoint(lgtm.getOtlpGrpcUrl())
+ .build();
+
+ BatchSpanProcessor spanProcessor = BatchSpanProcessor
+ .builder(spanExporter)
+ .setScheduleDelay(500, TimeUnit.MILLISECONDS)
+ .build();
+
+ SdkTracerProvider tracerProvider = SdkTracerProvider
+ .builder()
+ .addSpanProcessor(spanProcessor)
+ .setResource(Resource.create(Attributes.of(AttributeKey.stringKey("service.name"), "test-service")))
+ .build();
+
+ SdkLoggerProvider loggerProvider = SdkLoggerProvider
+ .builder()
+ .addLogRecordProcessor(SimpleLogRecordProcessor.create(logExporter))
+ .build();
+
+ OpenTelemetrySdk openTelemetry = OpenTelemetrySdk
+ .builder()
+ .setTracerProvider(tracerProvider)
+ .setLoggerProvider(loggerProvider)
+ .build();
+
+ String version = RestAssured
+ .get(String.format("http://%s:%s/api/health", lgtm.getHost(), lgtm.getMappedPort(3000)))
+ .jsonPath()
+ .get("version");
+ assertThat(version).isEqualTo("12.0.0");
+
+ OtlpConfig otlpConfig = createOtlpConfig(lgtm);
+ MeterRegistry meterRegistry = SystemStubs
+ .withEnvironmentVariable("OTEL_SERVICE_NAME", "testcontainers")
+ .execute(() -> new OtlpMeterRegistry(otlpConfig, Clock.SYSTEM));
+ Counter.builder("test.counter").register(meterRegistry).increment(2);
+
+ Logger logger = openTelemetry.getSdkLoggerProvider().loggerBuilder("test").build();
+ logger
+ .logRecordBuilder()
+ .setBody("Test log!")
+ .setAttribute(AttributeKey.stringKey("job"), "test-job")
+ .emit();
+
+ Tracer tracer = openTelemetry.getTracer("test");
+ Span span = tracer.spanBuilder("test").startSpan();
+ span.end();
+
+ Awaitility
+ .given()
+ .pollInterval(Duration.ofSeconds(2))
+ .atMost(Duration.ofSeconds(5))
+ .ignoreExceptions()
+ .untilAsserted(() -> {
+ Response metricResponse = RestAssured
+ .given()
+ .queryParam("query", "test_counter_total{job=\"testcontainers\"}")
+ .get(String.format("%s/api/v1/query", lgtm.getPrometheusHttpUrl()))
+ .prettyPeek()
+ .thenReturn();
+ assertThat(metricResponse.getStatusCode()).isEqualTo(200);
+ assertThat(metricResponse.body().jsonPath().getList("data.result[0].value")).contains("2");
+
+ Response logResponse = RestAssured
+ .given()
+ .queryParam("query", "{service_name=\"unknown_service:java\"}")
+ .get(String.format("%s/loki/api/v1/query_range", lgtm.getLokiUrl()))
+ .prettyPeek()
+ .thenReturn();
+ assertThat(logResponse.getStatusCode()).isEqualTo(200);
+ assertThat(logResponse.body().jsonPath().getString("data.result[0].values[0][1]"))
+ .isEqualTo("Test log!");
+
+ Response traceResponse = RestAssured
+ .given()
+ .get(String.format("%s/api/search", lgtm.getTempoUrl()))
+ .prettyPeek()
+ .thenReturn();
+ assertThat(traceResponse.getStatusCode()).isEqualTo(200);
+ assertThat(traceResponse.body().jsonPath().getString("traces[0].rootServiceName"))
+ .isEqualTo("test-service");
+ });
+
+ openTelemetry.close();
+ }
+ }
+
+ private static OtlpConfig createOtlpConfig(LgtmStackContainer lgtm) {
+ return new OtlpConfig() {
+ @Override
+ public String url() {
+ return String.format("%s/v1/metrics", lgtm.getOtlpHttpUrl());
+ }
+
+ @Override
+ public Duration step() {
+ return Duration.ofSeconds(1);
+ }
+
+ @Override
+ public String get(String s) {
+ return null;
+ }
+ };
+ }
+}
diff --git a/modules/grafana/src/test/resources/logback-test.xml b/modules/grafana/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..83ef7a1a3ef
--- /dev/null
+++ b/modules/grafana/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/modules/hivemq/build.gradle b/modules/hivemq/build.gradle
index 9f6be24e306..13ec90ff167 100644
--- a/modules/hivemq/build.gradle
+++ b/modules/hivemq/build.gradle
@@ -1,31 +1,24 @@
-description = "TestContainers :: HiveMQ"
+description = "Testcontainers :: HiveMQ"
dependencies {
api(project(":testcontainers"))
- api("org.jetbrains:annotations:23.0.0")
+ api("org.jetbrains:annotations:26.1.0")
- shaded("org.apache.commons:commons-lang3:3.12.0")
- shaded("commons-io:commons-io:2.11.0")
- shaded("org.javassist:javassist:3.29.2-GA")
+ shaded("org.apache.commons:commons-lang3:3.20.0")
+ shaded("commons-io:commons-io:2.21.0")
+ shaded("org.javassist:javassist:3.32.0-GA")
shaded("org.jboss.shrinkwrap:shrinkwrap-api:1.2.6")
shaded("org.jboss.shrinkwrap:shrinkwrap-impl-base:1.2.6")
- shaded("net.lingala.zip4j:zip4j:2.11.2")
+ shaded("net.lingala.zip4j:zip4j:2.11.6")
- testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.1")
- testImplementation(project(":junit-jupiter"))
- testImplementation("com.hivemq:hivemq-extension-sdk:4.9.0")
- testImplementation("com.hivemq:hivemq-mqtt-client:1.3.0")
- testImplementation("org.apache.httpcomponents:httpclient:4.5.13")
- testImplementation("ch.qos.logback:logback-classic:1.4.4")
- testImplementation 'org.assertj:assertj-core:3.23.1'
- testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.1")
+ testImplementation(project(":testcontainers-junit-jupiter"))
+ testImplementation("com.hivemq:hivemq-extension-sdk:4.50.0")
+ testImplementation("com.hivemq:hivemq-mqtt-client:1.3.15")
+ testImplementation("org.apache.httpcomponents:httpclient:4.5.14")
+ testImplementation("ch.qos.logback:logback-classic:1.5.37")
}
test {
- useJUnitPlatform()
- testLogging {
- events "passed", "skipped", "failed"
- }
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(11)
}
@@ -35,4 +28,5 @@ compileTestJava {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(11)
}
+ options.release.set(11)
}
diff --git a/modules/hivemq/src/main/java/org/testcontainers/hivemq/HiveMQContainer.java b/modules/hivemq/src/main/java/org/testcontainers/hivemq/HiveMQContainer.java
index 8a2832f65bd..241d5b18c9e 100644
--- a/modules/hivemq/src/main/java/org/testcontainers/hivemq/HiveMQContainer.java
+++ b/modules/hivemq/src/main/java/org/testcontainers/hivemq/HiveMQContainer.java
@@ -8,7 +8,7 @@
import org.slf4j.event.Level;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
@@ -29,6 +29,18 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+/**
+ * Testcontainers implementation for HiveMQ.
+ *
+ * Supported images: {@code hivemq/hivemq4}, {@code hivemq/hivemq-ce}
+ *
+ * Exposed ports:
+ *
+ * MQTT: 1883
+ * Control Center: 8080
+ * Debug: 9000
+ *
+ */
public class HiveMQContainer extends GenericContainer {
private static final Logger LOGGER = LoggerFactory.getLogger(HiveMQContainer.class);
@@ -70,7 +82,7 @@ public HiveMQContainer(final @NotNull DockerImageName dockerImageName) {
addExposedPort(MQTT_PORT);
- waitStrategy.withStrategy(new LogMessageWaitStrategy().withRegEx("(.*)Started HiveMQ in(.*)"));
+ waitStrategy.withStrategy(Wait.forLogMessage("(.*)Started HiveMQ in(.*)", 1));
waitingFor(waitStrategy);
withLogConsumer(outputFrame -> {
@@ -122,9 +134,9 @@ protected void configure() {
setCommand(
"-c",
removeCommand +
- "cp -r '/opt/hivemq/temp-extensions/'* /opt/hivemq/extensions/ " +
- "; chmod -R 777 /opt/hivemq/extensions " +
- "&& /opt/docker-entrypoint.sh /opt/hivemq/bin/run.sh"
+ "cp -r '/opt/hivemq/temp-extensions/'* /opt/hivemq/extensions/ ; " +
+ "chmod -R 777 /opt/hivemq/extensions ; " +
+ "/opt/docker-entrypoint.sh /opt/hivemq/bin/run.sh"
);
}
@@ -148,7 +160,7 @@ protected void containerIsStarted(final @NotNull InspectContainerResponse contai
*/
public @NotNull HiveMQContainer waitForExtension(final @NotNull String extensionName) {
final String regEX = "(.*)Extension \"" + extensionName + "\" version (.*) started successfully(.*)";
- waitStrategy.withStrategy(new LogMessageWaitStrategy().withRegEx(regEX));
+ waitStrategy.withStrategy(Wait.forLogMessage(regEX, 1));
return self();
}
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithControlCenterIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithControlCenterIT.java
index a3e8a20216d..6d122d824fd 100644
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithControlCenterIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithControlCenterIT.java
@@ -10,13 +10,13 @@
import java.util.concurrent.TimeUnit;
-public class ContainerWithControlCenterIT {
+class ContainerWithControlCenterIT {
public static final int CONTROL_CENTER_PORT = 8080;
@Test
@Timeout(value = 3, unit = TimeUnit.MINUTES)
- public void test() throws Exception {
+ void test() throws Exception {
try (
final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4"))
.withControlCenter()
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithCustomConfigIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithCustomConfigIT.java
index 3a3f2d13bb7..aed446db203 100755
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithCustomConfigIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithCustomConfigIT.java
@@ -13,7 +13,7 @@
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-public class ContainerWithCustomConfigIT {
+class ContainerWithCustomConfigIT {
@Test
@Timeout(value = 3, unit = TimeUnit.MINUTES)
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionFromDirectoryIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionFromDirectoryIT.java
index c1e6e72cc32..a5b6170a228 100755
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionFromDirectoryIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionFromDirectoryIT.java
@@ -1,7 +1,10 @@
package org.testcontainers.hivemq;
+import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.event.Level;
import org.testcontainers.hivemq.util.TestPublishModifiedUtil;
import org.testcontainers.utility.DockerImageName;
@@ -9,14 +12,20 @@
import java.util.concurrent.TimeUnit;
-public class ContainerWithExtensionFromDirectoryIT {
+class ContainerWithExtensionFromDirectoryIT {
- @Test
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "2020.1", // first version that provided a container image
+ "2024.3", // version that runs the image as a non-root user by default
+ }
+ )
@Timeout(value = 3, unit = TimeUnit.MINUTES)
- void test() throws Exception {
+ void test(final @NotNull String hivemqCeTag) throws Exception {
try (
final HiveMQContainer hivemq = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag)
)
.withExtension(MountableFile.forClasspathResource("/modifier-extension"))
.waitForExtension("Modifier Extension")
@@ -33,7 +42,7 @@ void test() throws Exception {
void test_wrongDirectoryName() throws Exception {
try (
final HiveMQContainer hivemq = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")
)
.withExtension(MountableFile.forClasspathResource("/modifier-extension-wrong-name"))
.waitForExtension("Modifier Extension")
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionIT.java
index 01a67e0dcfb..2b255912da9 100755
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionIT.java
@@ -1,7 +1,9 @@
package org.testcontainers.hivemq;
-import org.junit.jupiter.api.Test;
+import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.testcontainers.hivemq.util.MyExtension;
import org.testcontainers.hivemq.util.TestPublishModifiedUtil;
import org.testcontainers.utility.DockerImageName;
@@ -9,11 +11,17 @@
import java.util.concurrent.TimeUnit;
-public class ContainerWithExtensionIT {
+class ContainerWithExtensionIT {
- @Test
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "2020.1", // first version that provided a container image
+ "2024.3", // version that runs the image as a non-root user by default
+ }
+ )
@Timeout(value = 3, unit = TimeUnit.MINUTES)
- void test() throws Exception {
+ void test(final @NotNull String hivemqCeTag) throws Exception {
final HiveMQExtension hiveMQExtension = HiveMQExtension
.builder()
.id("extension-1")
@@ -24,7 +32,7 @@ void test() throws Exception {
try (
final HiveMQContainer hivemq = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag)
)
.withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml"))
.waitForExtension(hiveMQExtension)
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionSubclassIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionSubclassIT.java
index 1f3d9de4aeb..a907b8f2d2e 100755
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionSubclassIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionSubclassIT.java
@@ -10,7 +10,7 @@
import java.util.concurrent.TimeUnit;
-public class ContainerWithExtensionSubclassIT {
+class ContainerWithExtensionSubclassIT {
@Test
@Timeout(value = 3, unit = TimeUnit.MINUTES)
@@ -25,7 +25,7 @@ void test() throws Exception {
try (
final HiveMQContainer hivemq = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")
)
.waitForExtension(hiveMQExtension)
.withExtension(hiveMQExtension)
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInExtensionHomeIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInExtensionHomeIT.java
index a5b5238ce25..c3c74b0dce7 100755
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInExtensionHomeIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInExtensionHomeIT.java
@@ -9,8 +9,9 @@
import com.hivemq.extension.sdk.api.services.Services;
import com.hivemq.extension.sdk.api.services.intializer.ClientInitializer;
import org.jetbrains.annotations.NotNull;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.testcontainers.hivemq.util.TestPublishModifiedUtil;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
@@ -20,11 +21,17 @@
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
-public class ContainerWithFileInExtensionHomeIT {
+class ContainerWithFileInExtensionHomeIT {
- @Test
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "2020.1", // first version that provided a container image
+ "2024.3", // version that runs the image as a non-root user by default
+ }
+ )
@Timeout(value = 3, unit = TimeUnit.MINUTES)
- void test() throws Exception {
+ void test(final @NotNull String hivemqCeTag) throws Exception {
final HiveMQExtension hiveMQExtension = HiveMQExtension
.builder()
.id("extension-1")
@@ -35,7 +42,7 @@ void test() throws Exception {
try (
final HiveMQContainer hivemq = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag)
)
.withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml"))
.withExtension(hiveMQExtension)
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInHomeIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInHomeIT.java
index 2fd387f394c..c8e19316788 100755
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInHomeIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInHomeIT.java
@@ -20,7 +20,7 @@
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
-public class ContainerWithFileInHomeIT {
+class ContainerWithFileInHomeIT {
@Test
@Timeout(value = 3, unit = TimeUnit.MINUTES)
@@ -35,7 +35,7 @@ void test() throws Exception {
try (
final HiveMQContainer hivemq = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")
)
.withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml"))
.withExtension(hiveMQExtension)
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithLicenseIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithLicenseIT.java
index 9d82188c35c..3973abddb0b 100755
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithLicenseIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithLicenseIT.java
@@ -20,7 +20,7 @@
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
-public class ContainerWithLicenseIT {
+class ContainerWithLicenseIT {
@Test
@Timeout(value = 3, unit = TimeUnit.MINUTES)
@@ -35,7 +35,7 @@ void test() throws Exception {
try (
final HiveMQContainer hivemq = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")
)
.withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml"))
.withExtension(hiveMQExtension)
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithoutPlatformExtensionsIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithoutPlatformExtensionsIT.java
index fde18e09285..f29897e6a1e 100644
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithoutPlatformExtensionsIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithoutPlatformExtensionsIT.java
@@ -26,7 +26,7 @@
import static org.assertj.core.api.Assertions.assertThat;
-public class ContainerWithoutPlatformExtensionsIT {
+class ContainerWithoutPlatformExtensionsIT {
@NotNull
private final HiveMQExtension hiveMQExtension = HiveMQExtension
@@ -39,7 +39,7 @@ public class ContainerWithoutPlatformExtensionsIT {
@Test
@Timeout(value = 3, unit = TimeUnit.MINUTES)
- public void removeAllPlatformExtensions() throws InterruptedException {
+ void removeAllPlatformExtensions() throws InterruptedException {
try (
final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4"))
.withExtension(hiveMQExtension)
@@ -74,7 +74,7 @@ public void removeAllPlatformExtensions() throws InterruptedException {
@Test
@Timeout(value = 3, unit = TimeUnit.MINUTES)
- public void removeKafkaExtension() throws InterruptedException {
+ void removeKafkaExtension() throws InterruptedException {
try (
final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4"))
.withExtension(hiveMQExtension)
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInCopiedDirectoryIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInCopiedDirectoryIT.java
index cd83cb8a10d..df0f3ef3b9c 100755
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInCopiedDirectoryIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInCopiedDirectoryIT.java
@@ -24,7 +24,7 @@
import static org.assertj.core.api.Assertions.assertThat;
-public class CreateFileInCopiedDirectoryIT {
+class CreateFileInCopiedDirectoryIT {
private @NotNull MountableFile createDirectory() throws IOException {
final File directory = new File(Files.createTempDirectory("").toFile(), "directory");
@@ -47,7 +47,7 @@ void test() throws Exception {
try (
final HiveMQContainer hivemq = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")
)
.withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml"))
.withExtension(extension)
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInExtensionDirectoryIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInExtensionDirectoryIT.java
index 44f5f4f632d..6f65ebb1e9f 100755
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInExtensionDirectoryIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInExtensionDirectoryIT.java
@@ -9,8 +9,9 @@
import com.hivemq.extension.sdk.api.services.Services;
import com.hivemq.extension.sdk.api.services.intializer.ClientInitializer;
import org.jetbrains.annotations.NotNull;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.testcontainers.hivemq.util.TestPublishModifiedUtil;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
@@ -21,11 +22,17 @@
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
-public class CreateFileInExtensionDirectoryIT {
+class CreateFileInExtensionDirectoryIT {
- @Test
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "2020.1", // first version that provided a container image
+ "2024.3", // version that runs the image as a non-root user by default
+ }
+ )
@Timeout(value = 3, unit = TimeUnit.MINUTES)
- void test() throws Exception {
+ void test(final @NotNull String hivemqCeTag) throws Exception {
final HiveMQExtension hiveMQExtension = HiveMQExtension
.builder()
.id("extension-1")
@@ -36,7 +43,7 @@ void test() throws Exception {
try (
final HiveMQContainer hivemq = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag)
)
.withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml"))
.waitForExtension(hiveMQExtension)
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/DisableEnableExtensionFromDirectoryIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/DisableEnableExtensionFromDirectoryIT.java
index a2ee9edfbcf..b632bb45328 100644
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/DisableEnableExtensionFromDirectoryIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/DisableEnableExtensionFromDirectoryIT.java
@@ -12,7 +12,7 @@
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-public class DisableEnableExtensionFromDirectoryIT {
+class DisableEnableExtensionFromDirectoryIT {
@Test
@Timeout(value = 3, unit = TimeUnit.MINUTES)
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/DisableEnableExtensionIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/DisableEnableExtensionIT.java
index 361af190a57..189f34308a1 100644
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/DisableEnableExtensionIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/DisableEnableExtensionIT.java
@@ -13,7 +13,7 @@
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-public class DisableEnableExtensionIT {
+class DisableEnableExtensionIT {
@NotNull
private final HiveMQExtension hiveMQExtension = HiveMQExtension
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/HiveMQTestContainerCore.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/HiveMQTestContainerCore.java
index 32097e52641..ede972e957d 100644
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/HiveMQTestContainerCore.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/HiveMQTestContainerCore.java
@@ -16,7 +16,7 @@
class HiveMQTestContainerCore {
@NotNull
- final HiveMQContainer container = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3"));
+ final HiveMQContainer container = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3"));
@TempDir
File tempDir;
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java
index 97a1365ac91..b2ac3acbbcb 100644
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java
@@ -10,7 +10,7 @@
import org.testcontainers.utility.MountableFile;
@Testcontainers
-public class DemoDisableExtensionsIT {
+class DemoDisableExtensionsIT {
// noExtensions {
@Container
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoExtensionTestsIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoExtensionTestsIT.java
index e5385369d3a..a1ff664649d 100644
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoExtensionTestsIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoExtensionTestsIT.java
@@ -15,7 +15,7 @@
import java.util.concurrent.TimeUnit;
@Testcontainers
-public class DemoExtensionTestsIT {
+class DemoExtensionTestsIT {
// waitStrategy {
@Container
@@ -38,7 +38,7 @@ public class DemoExtensionTestsIT {
@Container
final HiveMQContainer hivemqWithClasspathExtension = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")
)
.waitForExtension(hiveMQEClasspathxtension)
.withExtension(hiveMQEClasspathxtension)
@@ -48,7 +48,7 @@ public class DemoExtensionTestsIT {
@Test
@Timeout(value = 3, unit = TimeUnit.MINUTES)
- public void test() throws Exception {
+ void test() throws Exception {
// mqtt5client {
final Mqtt5BlockingClient client = Mqtt5Client
.builder()
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java
index d0bf6fc6b9c..8fe08b24750 100644
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java
@@ -15,11 +15,11 @@
import java.util.concurrent.TimeUnit;
@Testcontainers
-public class DemoFilesIT {
+class DemoFilesIT {
// hivemqHome {
final HiveMQContainer hivemqFileInHome = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")
)
.withFileInHomeFolder(
MountableFile.forHostPath("src/test/resources/additionalFile.txt"),
@@ -31,7 +31,7 @@ public class DemoFilesIT {
// extensionHome {
@Container
final HiveMQContainer hivemqFileInExtensionHome = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")
)
.withExtension(
HiveMQExtension
@@ -52,7 +52,7 @@ public class DemoFilesIT {
// withLicenses {
@Container
- final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3"))
+ final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3"))
.withLicense(MountableFile.forHostPath("src/test/resources/myLicense.lic"))
.withLicense(MountableFile.forHostPath("src/test/resources/myExtensionLicense.elic"));
@@ -60,7 +60,7 @@ public class DemoFilesIT {
@Test
@Timeout(value = 3, unit = TimeUnit.MINUTES)
- public void test() throws Exception {
+ void test() throws Exception {
// mqtt5client {
final Mqtt5BlockingClient client = Mqtt5Client
.builder()
diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java
index be39fe6d7f2..ed063617537 100644
--- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java
+++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java
@@ -14,18 +14,18 @@
import java.util.concurrent.TimeUnit;
@Testcontainers
-public class DemoHiveMQContainerIT {
+class DemoHiveMQContainerIT {
// ceVersion {
@Container
- final HiveMQContainer hivemqCe = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3"))
+ final HiveMQContainer hivemqCe = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3"))
.withLogLevel(Level.DEBUG);
// }
// hiveEEVersion {
@Container
- final HiveMQContainer hivemqEe = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3"))
+ final HiveMQContainer hivemqEe = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4"))
.withLogLevel(Level.DEBUG);
// }
@@ -33,7 +33,7 @@ public class DemoHiveMQContainerIT {
// eeVersionWithControlCenter {
@Container
final HiveMQContainer hivemqEeWithControlCenter = new HiveMQContainer(
- DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")
+ DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4")
)
.withLogLevel(Level.DEBUG)
.withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml"))
@@ -43,13 +43,13 @@ public class DemoHiveMQContainerIT {
// specificVersion {
@Container
- final HiveMQContainer hivemqSpecificVersion = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce:2021.3"));
+ final HiveMQContainer hivemqSpecificVersion = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce:2024.3"));
// }
@Test
@Timeout(value = 3, unit = TimeUnit.MINUTES)
- public void test() throws Exception {
+ void test() throws Exception {
// mqtt5client {
final Mqtt5BlockingClient client = Mqtt5Client
.builder()
diff --git a/modules/influxdb/build.gradle b/modules/influxdb/build.gradle
index 2a75c343653..145aa4aac52 100644
--- a/modules/influxdb/build.gradle
+++ b/modules/influxdb/build.gradle
@@ -3,7 +3,8 @@ description = "Testcontainers :: InfluxDB"
dependencies {
api project(':testcontainers')
- compileOnly 'org.influxdb:influxdb-java:2.23'
- testImplementation 'org.influxdb:influxdb-java:2.23'
- testImplementation 'org.assertj:assertj-core:3.23.1'
+ compileOnly 'org.influxdb:influxdb-java:2.25'
+
+ testImplementation 'org.influxdb:influxdb-java:2.25'
+ testImplementation "com.influxdb:influxdb-client-java:7.5.0"
}
diff --git a/modules/influxdb/src/main/java/org/testcontainers/containers/InfluxDBContainer.java b/modules/influxdb/src/main/java/org/testcontainers/containers/InfluxDBContainer.java
index 40eea721b57..8e908e8e229 100644
--- a/modules/influxdb/src/main/java/org/testcontainers/containers/InfluxDBContainer.java
+++ b/modules/influxdb/src/main/java/org/testcontainers/containers/InfluxDBContainer.java
@@ -1,16 +1,22 @@
package org.testcontainers.containers;
+import lombok.Getter;
import org.influxdb.InfluxDB;
import org.influxdb.InfluxDBFactory;
-import org.testcontainers.containers.wait.strategy.Wait;
-import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
+import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
+import org.testcontainers.utility.ComparableVersion;
import org.testcontainers.utility.DockerImageName;
import java.util.Collections;
+import java.util.Optional;
import java.util.Set;
/**
- * See https://store.docker.com/images/influxdb
+ * Testcontainers implementation for InfluxDB.
+ *
+ * Supported image: {@code influxdb}
+ *
+ * Exposed ports: 8086
*/
public class InfluxDBContainer> extends GenericContainer {
@@ -23,20 +29,45 @@ public class InfluxDBContainer> extends Gen
@Deprecated
public static final String VERSION = DEFAULT_TAG;
+ private static final int NO_CONTENT_STATUS_CODE = 204;
+
+ @Getter
+ private String username = "test-user";
+
+ @Getter
+ private String password = "test-password";
+
+ /**
+ * Properties of InfluxDB 1.x
+ */
private boolean authEnabled = true;
private String admin = "admin";
private String adminPassword = "password";
+ @Getter
private String database;
- private String username = "any";
+ /**
+ * Properties of InfluxDB 2.x
+ */
+ @Getter
+ private String bucket = "test-bucket";
+
+ @Getter
+ private String organization = "test-org";
+
+ @Getter
+ private Optional retention = Optional.empty();
- private String password = "any";
+ @Getter
+ private Optional adminToken = Optional.empty();
+
+ private final boolean isAtLeastMajorVersion2;
/**
- * @deprecated use {@link InfluxDBContainer(DockerImageName)} instead
+ * @deprecated use {@link #InfluxDBContainer(DockerImageName)} instead
*/
@Deprecated
public InfluxDBContainer() {
@@ -44,7 +75,7 @@ public InfluxDBContainer() {
}
/**
- * @deprecated use {@link InfluxDBContainer(DockerImageName)} instead
+ * @deprecated use {@link #InfluxDBContainer(DockerImageName)} instead
*/
@Deprecated
public InfluxDBContainer(final String version) {
@@ -55,24 +86,61 @@ public InfluxDBContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
- waitStrategy =
- new WaitAllStrategy()
- .withStrategy(Wait.forHttp("/ping").withBasicCredentials(username, password).forStatusCode(204))
- .withStrategy(Wait.forListeningPort());
+ this.waitStrategy =
+ new HttpWaitStrategy()
+ .forPath("/ping")
+ .withBasicCredentials(this.username, this.password)
+ .forStatusCode(NO_CONTENT_STATUS_CODE);
+ this.isAtLeastMajorVersion2 =
+ new ComparableVersion(dockerImageName.getVersionPart()).isGreaterThanOrEqualTo("2.0.0");
addExposedPort(INFLUXDB_PORT);
}
+ /**
+ * Sets the InfluxDB environment variables based on the version
+ */
@Override
protected void configure() {
- addEnv("INFLUXDB_ADMIN_USER", admin);
- addEnv("INFLUXDB_ADMIN_PASSWORD", adminPassword);
+ if (this.isAtLeastMajorVersion2) {
+ configureInfluxDBV2();
+ } else {
+ configureInfluxDBV1();
+ }
+ }
- addEnv("INFLUXDB_HTTP_AUTH_ENABLED", String.valueOf(authEnabled));
+ /**
+ * Sets the InfluxDB 2.x environment variables
+ *
+ * @see InfluxDB Dockerhub for full documentation on InfluxDB's
+ * envrinoment variables
+ */
+ private void configureInfluxDBV2() {
+ addEnv("DOCKER_INFLUXDB_INIT_MODE", "setup");
+
+ addEnv("DOCKER_INFLUXDB_INIT_USERNAME", this.username);
+ addEnv("DOCKER_INFLUXDB_INIT_PASSWORD", this.password);
+
+ addEnv("DOCKER_INFLUXDB_INIT_ORG", this.organization);
+ addEnv("DOCKER_INFLUXDB_INIT_BUCKET", this.bucket);
- addEnv("INFLUXDB_DB", database);
- addEnv("INFLUXDB_USER", username);
- addEnv("INFLUXDB_USER_PASSWORD", password);
+ this.retention.ifPresent(ret -> addEnv("DOCKER_INFLUXDB_INIT_RETENTION", ret));
+ this.adminToken.ifPresent(token -> addEnv("DOCKER_INFLUXDB_INIT_ADMIN_TOKEN", token));
+ }
+
+ /**
+ * Sets the InfluxDB 1.x environment variables
+ */
+ private void configureInfluxDBV1() {
+ addEnv("INFLUXDB_USER", this.username);
+ addEnv("INFLUXDB_USER_PASSWORD", this.password);
+
+ addEnv("INFLUXDB_HTTP_AUTH_ENABLED", String.valueOf(this.authEnabled));
+
+ addEnv("INFLUXDB_ADMIN_USER", this.admin);
+ addEnv("INFLUXDB_ADMIN_PASSWORD", this.adminPassword);
+
+ addEnv("INFLUXDB_DB", this.database);
}
@Override
@@ -81,87 +149,131 @@ public Set getLivenessCheckPortNumbers() {
}
/**
- * Set env variable `INFLUXDB_HTTP_AUTH_ENABLED`.
+ * Set user for InfluxDB
+ *
+ * @param username The username to set for the system's initial super-user
+ * @return a reference to this container instance
+ */
+ public InfluxDBContainer withUsername(final String username) {
+ this.username = username;
+ return this;
+ }
+
+ /**
+ * Set password for InfluxDB
+ *
+ * @param password The password to set for the system's initial super-user
+ * @return a reference to this container instance
+ */
+ public InfluxDBContainer withPassword(final String password) {
+ this.password = password;
+ return this;
+ }
+
+ /**
+ * Determines if authentication should be enabled or not
*
* @param authEnabled Enables authentication.
* @return a reference to this container instance
*/
- public SELF withAuthEnabled(final boolean authEnabled) {
+ public InfluxDBContainer withAuthEnabled(final boolean authEnabled) {
this.authEnabled = authEnabled;
- return self();
+ return this.self();
}
/**
- * Set env variable `INFLUXDB_ADMIN_USER`.
+ * Sets the admin user
*
* @param admin The name of the admin user to be created. If this is unset, no admin user is created.
* @return a reference to this container instance
*/
- public SELF withAdmin(final String admin) {
+ public InfluxDBContainer withAdmin(final String admin) {
this.admin = admin;
- return self();
+ return this.self();
}
/**
- * Set env variable `INFLUXDB_ADMIN_PASSWORD`.
+ * Sets the admin password
*
- * @param adminPassword TThe password for the admin user configured with `INFLUXDB_ADMIN_USER`. If this is unset, a
- * random password is generated and printed to standard out.
+ * @param adminPassword The password for the admin user. If this is unset, a random password is generated and
+ * printed to standard out.
* @return a reference to this container instance
*/
- public SELF withAdminPassword(final String adminPassword) {
+ public InfluxDBContainer withAdminPassword(final String adminPassword) {
this.adminPassword = adminPassword;
- return self();
+ return this.self();
}
/**
- * Set env variable `INFLUXDB_DB`.
+ * Initializes database with given name
*
- * @param database Automatically initializes a database with the name of this environment variable.
+ * @param database name of the database.
* @return a reference to this container instance
*/
- public SELF withDatabase(final String database) {
+ public InfluxDBContainer withDatabase(final String database) {
this.database = database;
- return self();
+ return this.self();
}
/**
- * Set env variable `INFLUXDB_USER`.
+ * Sets the organization name
*
- * @param username The name of a user to be created with no privileges. If `INFLUXDB_DB` is set, this user will
- * be granted read and write permissions for that database.
+ * @param organization The organization for the initial setup of influxDB.
* @return a reference to this container instance
*/
- public SELF withUsername(final String username) {
- this.username = username;
- return self();
+ public InfluxDBContainer withOrganization(final String organization) {
+ this.organization = organization;
+ return this;
}
/**
- * Set env variable `INFLUXDB_USER_PASSWORD`.
+ * Initializes bucket with given name
*
- * @param password The password for the user configured with `INFLUXDB_USER`. If this is unset, a random password
- * is generated and printed to standard out.
+ * @param bucket name of the bucket.
* @return a reference to this container instance
*/
- public SELF withPassword(final String password) {
- this.password = password;
- return self();
+ public InfluxDBContainer withBucket(final String bucket) {
+ this.bucket = bucket;
+ return this;
}
/**
- * @return a url to influxDb
+ * Sets the retention in days
+ *
+ * @param retention days bucket will retain data (0 is infinite, default is 0).
+ * @return a reference to this container instance
+ */
+ public InfluxDBContainer withRetention(final String retention) {
+ this.retention = Optional.of(retention);
+ return this;
+ }
+
+ /**
+ * Sets the admin token
+ *
+ * @param adminToken Authentication token to associate with the admin user.
+ * @return a reference to this container instance
+ */
+ public InfluxDBContainer withAdminToken(final String adminToken) {
+ this.adminToken = Optional.of(adminToken);
+ return this;
+ }
+
+ /**
+ * @return a url to InfluxDB
*/
public String getUrl() {
- return "http://" + getHost() + ":" + getLivenessCheckPort();
+ return "http://" + getHost() + ":" + getMappedPort(INFLUXDB_PORT);
}
/**
- * @return a influxDb client
+ * @return a InfluxDB client for InfluxDB 1.x.
+ * @deprecated Use the new InfluxDB client library.
*/
+ @Deprecated
public InfluxDB getNewInfluxDB() {
- InfluxDB influxDB = InfluxDBFactory.connect(getUrl(), username, password);
- influxDB.setDatabase(database);
+ final InfluxDB influxDB = InfluxDBFactory.connect(getUrl(), this.username, this.password);
+ influxDB.setDatabase(this.database);
return influxDB;
}
}
diff --git a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java
index bcf3d21f25b..53678fa6dce 100644
--- a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java
+++ b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java
@@ -1,44 +1,166 @@
package org.testcontainers.containers;
-import org.influxdb.InfluxDB;
-import org.junit.ClassRule;
-import org.junit.Test;
+import com.influxdb.client.InfluxDBClient;
+import com.influxdb.client.InfluxDBClientFactory;
+import com.influxdb.client.InfluxDBClientOptions;
+import com.influxdb.client.QueryApi;
+import com.influxdb.client.WriteApi;
+import com.influxdb.client.domain.Bucket;
+import com.influxdb.client.domain.BucketRetentionRules;
+import com.influxdb.client.domain.WritePrecision;
+import com.influxdb.client.write.Point;
+import com.influxdb.query.FluxRecord;
+import com.influxdb.query.FluxTable;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.utility.DockerImageName;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
-public class InfluxDBContainerTest {
+class InfluxDBContainerTest {
+
+ private static final String USERNAME = "new-test-user";
+
+ private static final String PASSWORD = "new-test-password";
+
+ private static final String ORG = "new-test-org";
+
+ private static final String BUCKET = "new-test-bucket";
- @ClassRule
- public static InfluxDBContainer> influxDBContainer = new InfluxDBContainer<>(
- InfluxDBTestImages.INFLUXDB_TEST_IMAGE
- );
+ private static final String RETENTION = "1w";
+
+ private static final String ADMIN_TOKEN = "super-secret-token";
+
+ private static final int SECONDS_IN_WEEK = 604800;
@Test
- public void getUrl() {
- String actual = influxDBContainer.getUrl();
+ void getInfluxDBClient() {
+ try (
+ // constructorWithDefaultVariables {
+ final InfluxDBContainer> influxDBContainer = new InfluxDBContainer<>(
+ DockerImageName.parse("influxdb:2.0.7")
+ )
+ // }
+ ) {
+ influxDBContainer.start();
- assertThat(actual).isNotNull();
+ try (final InfluxDBClient influxDBClient = createClient(influxDBContainer)) {
+ assertThat(influxDBClient).isNotNull();
+ assertThat(influxDBClient.ping()).isTrue();
+ }
+ }
}
@Test
- public void getNewInfluxDB() {
- InfluxDB actual = influxDBContainer.getNewInfluxDB();
+ void getInfluxDBClientWithAdminToken() {
+ try (
+ // constructorWithAdminToken {
+ final InfluxDBContainer> influxDBContainer = new InfluxDBContainer<>(
+ DockerImageName.parse("influxdb:2.0.7")
+ )
+ .withAdminToken(ADMIN_TOKEN)
+ // }
+ ) {
+ influxDBContainer.start();
+ final Optional adminToken = influxDBContainer.getAdminToken();
+ assertThat(adminToken).isNotEmpty();
- assertThat(actual).isNotNull();
- assertThat(actual.ping()).isNotNull();
+ try (
+ final InfluxDBClient influxDBClient = createClientWithToken(
+ influxDBContainer.getUrl(),
+ adminToken.get()
+ )
+ ) {
+ assertThat(influxDBClient).isNotNull();
+ assertThat(influxDBClient.ping()).isTrue();
+ }
+ }
}
@Test
- public void getLivenessCheckPort() {
- Integer actual = influxDBContainer.getLivenessCheckPort();
+ void getBucket() {
+ try (
+ // constructorWithCustomVariables {
+ final InfluxDBContainer> influxDBContainer = new InfluxDBContainer<>(
+ DockerImageName.parse("influxdb:2.0.7")
+ )
+ .withUsername(USERNAME)
+ .withPassword(PASSWORD)
+ .withOrganization(ORG)
+ .withBucket(BUCKET)
+ .withRetention(RETENTION);
+ // }
+ ) {
+ influxDBContainer.start();
- assertThat(actual).isNotNull();
+ try (final InfluxDBClient influxDBClient = createClient(influxDBContainer)) {
+ final Bucket bucket = influxDBClient.getBucketsApi().findBucketByName(BUCKET);
+ assertThat(bucket).isNotNull();
+
+ assertThat(bucket.getName()).isEqualTo(BUCKET);
+ assertThat(bucket.getRetentionRules())
+ .hasSize(1)
+ .first()
+ .extracting(BucketRetentionRules::getEverySeconds)
+ .isEqualTo(SECONDS_IN_WEEK);
+ }
+ }
}
@Test
- public void isRunning() {
- boolean actual = influxDBContainer.isRunning();
+ void queryForWriteAndRead() {
+ try (
+ final InfluxDBContainer> influxDBContainer = new InfluxDBContainer<>(
+ InfluxDBTestUtils.INFLUXDB_V2_TEST_IMAGE
+ )
+ .withUsername(USERNAME)
+ .withPassword(PASSWORD)
+ .withOrganization(ORG)
+ .withBucket(BUCKET)
+ .withRetention(RETENTION)
+ ) {
+ influxDBContainer.start();
+
+ try (final InfluxDBClient influxDBClient = createClient(influxDBContainer)) {
+ try (final WriteApi writeApi = influxDBClient.makeWriteApi()) {
+ final Point point = Point
+ .measurement("temperature")
+ .addTag("location", "west")
+ .addField("value", 55.0D)
+ .time(Instant.now().toEpochMilli(), WritePrecision.MS);
+
+ writeApi.writePoint(point);
+ }
+
+ final String flux = String.format("from(bucket:\"%s\") |> range(start: 0)", BUCKET);
+
+ final QueryApi queryApi = influxDBClient.getQueryApi();
+
+ final FluxTable fluxTable = queryApi.query(flux).get(0);
+ final List records = fluxTable.getRecords();
+ assertThat(records).hasSize(1);
+ }
+ }
+ }
+
+ // createInfluxDBClient {
+ public static InfluxDBClient createClient(final InfluxDBContainer> influxDBContainer) {
+ final InfluxDBClientOptions influxDBClientOptions = InfluxDBClientOptions
+ .builder()
+ .url(influxDBContainer.getUrl())
+ .authenticate(influxDBContainer.getUsername(), influxDBContainer.getPassword().toCharArray())
+ .bucket(influxDBContainer.getBucket())
+ .org(influxDBContainer.getOrganization())
+ .build();
+ return InfluxDBClientFactory.create(influxDBClientOptions);
+ }
+
+ // }
- assertThat(actual).isTrue();
+ public static InfluxDBClient createClientWithToken(final String url, final String token) {
+ return InfluxDBClientFactory.create(url, token.toCharArray());
}
}
diff --git a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerV1Test.java b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerV1Test.java
new file mode 100644
index 00000000000..ea074cee68e
--- /dev/null
+++ b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerV1Test.java
@@ -0,0 +1,129 @@
+package org.testcontainers.containers;
+
+import org.influxdb.InfluxDB;
+import org.influxdb.InfluxDBFactory;
+import org.influxdb.dto.Point;
+import org.influxdb.dto.Query;
+import org.influxdb.dto.QueryResult;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class InfluxDBContainerV1Test {
+
+ private static final String TEST_VERSION = InfluxDBTestUtils.INFLUXDB_V1_TEST_IMAGE.getVersionPart();
+
+ private static final String DATABASE = "test";
+
+ private static final String USER = "new-test-user";
+
+ private static final String PASSWORD = "new-test-password";
+
+ @Test
+ void createInfluxDBOnlyWithUrlAndCorrectVersion() {
+ try (
+ // constructorWithDefaultVariables {
+ final InfluxDBContainer> influxDBContainer = new InfluxDBContainer<>(
+ DockerImageName.parse("influxdb:1.4.3")
+ )
+ // }
+ ) {
+ // Start the container. This step might take some time...
+ influxDBContainer.start();
+
+ try (final InfluxDB influxDBClient = createInfluxDBWithUrl(influxDBContainer)) {
+ assertThat(influxDBClient).isNotNull();
+ assertThat(influxDBClient.ping().isGood()).isTrue();
+ assertThat(influxDBClient.version()).isEqualTo(TEST_VERSION);
+ }
+ }
+ }
+
+ @Test
+ void getNewInfluxDBWithCorrectVersion() {
+ try (
+ final InfluxDBContainer> influxDBContainer = new InfluxDBContainer<>(
+ InfluxDBTestUtils.INFLUXDB_V1_TEST_IMAGE
+ )
+ ) {
+ // Start the container. This step might take some time...
+ influxDBContainer.start();
+
+ try (final InfluxDB influxDBClient = createInfluxDBWithUrl(influxDBContainer)) {
+ assertThat(influxDBClient).isNotNull();
+ assertThat(influxDBClient.ping().isGood()).isTrue();
+ assertThat(influxDBClient.version()).isEqualTo(TEST_VERSION);
+ }
+ }
+ }
+
+ @Test
+ void describeDatabases() {
+ try (
+ // constructorWithUserPassword {
+ final InfluxDBContainer> influxDBContainer = new InfluxDBContainer<>(
+ DockerImageName.parse("influxdb:1.4.3")
+ )
+ .withDatabase(DATABASE)
+ .withUsername(USER)
+ .withPassword(PASSWORD)
+ // }
+ ) {
+ // Start the container. This step might take some time...
+ influxDBContainer.start();
+
+ try (final InfluxDB influxDBClient = createInfluxDBWithUrl(influxDBContainer)) {
+ assertThat(influxDBClient.describeDatabases()).contains(DATABASE);
+ }
+ }
+ }
+
+ @Test
+ void queryForWriteAndRead() {
+ try (
+ final InfluxDBContainer> influxDBContainer = new InfluxDBContainer<>(
+ InfluxDBTestUtils.INFLUXDB_V1_TEST_IMAGE
+ )
+ .withDatabase(DATABASE)
+ .withUsername(USER)
+ .withPassword(PASSWORD)
+ ) {
+ // Start the container. This step might take some time...
+ influxDBContainer.start();
+
+ try (final InfluxDB influxDBClient = createInfluxDBWithUrl(influxDBContainer)) {
+ final Point point = Point
+ .measurement("cpu")
+ .time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+ .addField("idle", 90L)
+ .addField("user", 9L)
+ .addField("system", 1L)
+ .build();
+ influxDBClient.write(point);
+
+ final Query query = new Query("SELECT idle FROM cpu", DATABASE);
+ final QueryResult actual = influxDBClient.query(query);
+
+ assertThat(actual).isNotNull();
+ assertThat(actual.getError()).isNull();
+ assertThat(actual.getResults()).isNotNull();
+ assertThat(actual.getResults()).hasSize(1);
+ }
+ }
+ }
+
+ // createInfluxDBClient {
+ public static InfluxDB createInfluxDBWithUrl(final InfluxDBContainer> container) {
+ InfluxDB influxDB = InfluxDBFactory.connect(
+ container.getUrl(),
+ container.getUsername(),
+ container.getPassword()
+ );
+ influxDB.setDatabase(container.getDatabase());
+ return influxDB;
+ }
+ // }
+}
diff --git a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerWithUserTest.java b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerWithUserTest.java
deleted file mode 100644
index 7a34bbb4fd6..00000000000
--- a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerWithUserTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package org.testcontainers.containers;
-
-import org.influxdb.InfluxDB;
-import org.influxdb.dto.Point;
-import org.influxdb.dto.Query;
-import org.influxdb.dto.QueryResult;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.util.concurrent.TimeUnit;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class InfluxDBContainerWithUserTest {
-
- private static final String TEST_VERSION = InfluxDBTestImages.INFLUXDB_TEST_IMAGE.getVersionPart();
-
- private static final String DATABASE = "test";
-
- private static final String USER = "test-user";
-
- private static final String PASSWORD = "test-password";
-
- @Rule
- public InfluxDBContainer> influxDBContainer = new InfluxDBContainer<>(InfluxDBTestImages.INFLUXDB_TEST_IMAGE)
- .withDatabase(DATABASE)
- .withUsername(USER)
- .withPassword(PASSWORD);
-
- @Test
- public void describeDatabases() {
- InfluxDB actual = influxDBContainer.getNewInfluxDB();
-
- assertThat(actual).isNotNull();
- assertThat(actual.describeDatabases()).contains(DATABASE);
- }
-
- @Test
- public void checkVersion() {
- InfluxDB actual = influxDBContainer.getNewInfluxDB();
-
- assertThat(actual).isNotNull();
-
- assertThat(actual.ping()).isNotNull();
- assertThat(actual.ping().getVersion()).isEqualTo(TEST_VERSION);
-
- assertThat(actual.version()).isEqualTo(TEST_VERSION);
- }
-
- @Test
- public void queryForWriteAndRead() {
- InfluxDB influxDB = influxDBContainer.getNewInfluxDB();
-
- Point point = Point
- .measurement("cpu")
- .time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
- .addField("idle", 90L)
- .addField("user", 9L)
- .addField("system", 1L)
- .build();
- influxDB.write(point);
-
- Query query = new Query("SELECT idle FROM cpu", DATABASE);
- QueryResult actual = influxDB.query(query);
-
- assertThat(actual).isNotNull();
- assertThat(actual.getError()).isNull();
- assertThat(actual.getResults()).isNotNull();
- assertThat(actual.getResults()).hasSize(1);
- }
-}
diff --git a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBTestImages.java b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBTestImages.java
deleted file mode 100644
index a0e2d31bbdf..00000000000
--- a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBTestImages.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.testcontainers.containers;
-
-import org.testcontainers.utility.DockerImageName;
-
-public interface InfluxDBTestImages {
- DockerImageName INFLUXDB_TEST_IMAGE = DockerImageName.parse("influxdb:1.4.3");
-}
diff --git a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBTestUtils.java b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBTestUtils.java
new file mode 100644
index 00000000000..db9ae2bf79f
--- /dev/null
+++ b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBTestUtils.java
@@ -0,0 +1,10 @@
+package org.testcontainers.containers;
+
+import org.testcontainers.utility.DockerImageName;
+
+public final class InfluxDBTestUtils {
+
+ static final DockerImageName INFLUXDB_V1_TEST_IMAGE = DockerImageName.parse("influxdb:1.4.3");
+
+ static final DockerImageName INFLUXDB_V2_TEST_IMAGE = DockerImageName.parse("influxdb:2.0.7");
+}
diff --git a/modules/influxdb/src/test/resources/logback-test.xml b/modules/influxdb/src/test/resources/logback-test.xml
index 535e406fc13..83ef7a1a3ef 100644
--- a/modules/influxdb/src/test/resources/logback-test.xml
+++ b/modules/influxdb/src/test/resources/logback-test.xml
@@ -12,5 +12,5 @@
-
+
diff --git a/modules/jdbc-test/build.gradle b/modules/jdbc-test/build.gradle
index ea170edbd82..44fe705a255 100644
--- a/modules/jdbc-test/build.gradle
+++ b/modules/jdbc-test/build.gradle
@@ -1,17 +1,15 @@
dependencies {
- api project(':jdbc')
- api project(':test-support')
+ api project(':testcontainers-jdbc')
- api 'com.google.guava:guava:31.1-jre'
- api 'org.apache.commons:commons-lang3:3.12.0'
+ api 'com.google.guava:guava:33.5.0-jre'
+ api 'org.apache.commons:commons-lang3:3.20.0'
api 'com.zaxxer:HikariCP-java6:2.3.13'
- api 'commons-dbutils:commons-dbutils:1.7'
+ api 'commons-dbutils:commons-dbutils:1.8.1'
- api 'com.googlecode.junit-toolbox:junit-toolbox:2.4'
+ api 'org.assertj:assertj-core:3.27.7'
- api 'org.assertj:assertj-core:3.23.1'
-
- api 'org.apache.tomcat:tomcat-jdbc:10.0.27'
- api 'org.vibur:vibur-dbcp:25.0'
- api 'mysql:mysql-connector-java:8.0.31'
+ api 'org.apache.tomcat:tomcat-jdbc:11.0.23'
+ api 'org.vibur:vibur-dbcp:26.0'
+ api 'com.mysql:mysql-connector-j:9.6.0'
+ api 'org.junit.jupiter:junit-jupiter:5.14.3'
}
diff --git a/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java b/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java
index a152a1ae550..95f9d5d1ad4 100644
--- a/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java
+++ b/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java
@@ -4,9 +4,11 @@
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.lang3.SystemUtils;
-import org.junit.AfterClass;
-import org.junit.Test;
-import org.junit.runners.Parameterized.Parameter;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.Parameter;
+import org.junit.jupiter.params.ParameterizedClass;
+import org.junit.jupiter.params.provider.MethodSource;
import java.sql.Connection;
import java.sql.ResultSet;
@@ -15,8 +17,10 @@
import java.util.EnumSet;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assume.assumeFalse;
+import static org.assertj.core.api.Assumptions.assumeThat;
+@ParameterizedClass
+@MethodSource("data")
public class AbstractJDBCDriverTest {
protected enum Options {
@@ -27,7 +31,7 @@ protected enum Options {
PmdKnownBroken,
}
- @Parameter
+ @Parameter(0)
public String jdbcUrl;
@Parameter(1)
@@ -39,13 +43,13 @@ public static void sampleInitFunction(Connection connection) throws SQLException
connection.createStatement().execute("CREATE TABLE my_counter (\n" + " n INT\n" + ");");
}
- @AfterClass
+ @AfterAll
public static void testCleanup() {
ContainerDatabaseDriver.killContainers();
}
@Test
- public void test() throws SQLException {
+ void test() throws SQLException {
try (HikariDataSource dataSource = getDataSource(jdbcUrl, 1)) {
performSimpleTest(dataSource);
@@ -130,7 +134,8 @@ private void performTestForJDBCParamUsage(HikariDataSource dataSource) throws SQ
if (
databaseType.equalsIgnoreCase("postgresql") ||
databaseType.equalsIgnoreCase("postgis") ||
- databaseType.equalsIgnoreCase("timescaledb")
+ databaseType.equalsIgnoreCase("timescaledb") ||
+ databaseType.equalsIgnoreCase("pgvector")
) {
databaseQuery = "SELECT CURRENT_DATABASE()";
}
@@ -202,15 +207,15 @@ private HikariDataSource verifyCharacterSet(String jdbcUrl) throws SQLException
}
private void performTestForCustomIniFile(HikariDataSource dataSource) throws SQLException {
- assumeFalse(SystemUtils.IS_OS_WINDOWS);
+ assumeThat(SystemUtils.IS_OS_WINDOWS).isFalse();
Statement statement = dataSource.getConnection().createStatement();
- statement.execute("SELECT @@GLOBAL.innodb_file_format");
+ statement.execute("SELECT @@GLOBAL.innodb_max_undo_log_size");
ResultSet resultSet = statement.getResultSet();
assertThat(resultSet.next()).as("The query returns a result").isTrue();
- String result = resultSet.getString(1);
+ long result = resultSet.getLong(1);
- assertThat(result).as("The InnoDB file format has been set by the ini file content").isEqualTo("Barracuda");
+ assertThat(result).as("The InnoDB max undo log size has been set by the ini file content").isEqualTo(20000000);
}
private HikariDataSource getDataSource(String jdbcUrl, int poolSize) {
diff --git a/modules/jdbc-test/src/main/resources/logback-test.xml b/modules/jdbc-test/src/main/resources/logback-test.xml
index 535e406fc13..83ef7a1a3ef 100644
--- a/modules/jdbc-test/src/main/resources/logback-test.xml
+++ b/modules/jdbc-test/src/main/resources/logback-test.xml
@@ -12,5 +12,5 @@
-
+
diff --git a/modules/jdbc/build.gradle b/modules/jdbc/build.gradle
index 8ec50228a2e..4d538fe2720 100644
--- a/modules/jdbc/build.gradle
+++ b/modules/jdbc/build.gradle
@@ -1,16 +1,14 @@
description = "Testcontainers :: JDBC"
dependencies {
- api project(':database-commons')
- testImplementation project(':test-support')
+ api project(':testcontainers-database-commons')
- compileOnly 'org.jetbrains:annotations:23.0.0'
- testImplementation 'commons-dbutils:commons-dbutils:1.7'
- testImplementation 'org.vibur:vibur-dbcp:25.0'
- testImplementation 'org.apache.tomcat:tomcat-jdbc:10.1.1'
+ compileOnly 'org.jetbrains:annotations:26.1.0'
+ testImplementation 'commons-dbutils:commons-dbutils:1.8.1'
+ testImplementation 'org.vibur:vibur-dbcp:26.0'
+ testImplementation 'org.apache.tomcat:tomcat-jdbc:11.0.21'
testImplementation 'com.zaxxer:HikariCP-java6:2.3.13'
- testImplementation 'org.assertj:assertj-core:3.23.1'
- testImplementation ('org.mockito:mockito-core:4.8.1') {
+ testImplementation ('org.mockito:mockito-core:4.11.0') {
exclude(module: 'hamcrest-core')
}
}
diff --git a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java
index 88c418e73cb..cf6c995528f 100644
--- a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java
+++ b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java
@@ -5,6 +5,7 @@
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import org.testcontainers.containers.traits.LinkableContainer;
import org.testcontainers.delegate.DatabaseDelegate;
import org.testcontainers.ext.ScriptUtils;
@@ -16,16 +17,19 @@
import java.sql.Driver;
import java.sql.SQLException;
import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Base class for containers that expose a JDBC connection
- *
- * @author richardnorth
*/
public abstract class JdbcDatabaseContainer>
extends GenericContainer
@@ -35,7 +39,7 @@ public abstract class JdbcDatabaseContainer initScriptPaths = new ArrayList<>();
protected Map parameters = new HashMap<>();
@@ -48,7 +52,7 @@ public abstract class JdbcDatabaseContainer();
+ this.initScriptPaths.add(initScriptPath);
+ return self();
+ }
+
+ /**
+ * Sets an ordered array of scripts for initialization.
+ *
+ * @param initScriptPaths paths to the script files
+ * @return self
+ */
+ public SELF withInitScripts(String... initScriptPaths) {
+ return withInitScripts(Arrays.asList(initScriptPaths));
+ }
+
+ /**
+ * Sets an ordered collection of scripts for initialization.
+ *
+ * @param initScriptPaths paths to the script files
+ * @return self
+ */
+ public SELF withInitScripts(Iterable initScriptPaths) {
+ this.initScriptPaths = new ArrayList<>();
+ initScriptPaths.forEach(this.initScriptPaths::add);
return self();
}
@@ -149,17 +182,16 @@ protected void waitUntilContainerStarted() {
);
// Repeatedly try and open a connection to the DB and execute a test query
- long start = System.currentTimeMillis();
+ long start = System.nanoTime();
Exception lastConnectionException = null;
- while (System.currentTimeMillis() < start + (1000 * startupTimeoutSeconds)) {
+ while ((System.nanoTime() - start) < TimeUnit.SECONDS.toNanos(startupTimeoutSeconds)) {
if (!isRunning()) {
Thread.sleep(100L);
} else {
try (Connection connection = createConnection(""); Statement statement = connection.createStatement()) {
boolean testQuerySucceeded = statement.execute(this.getTestQueryString());
if (testQuerySucceeded) {
- logger().info("Container is started (JDBC URL: {})", this.getJdbcUrl());
return;
}
} catch (NoDriverFoundException e) {
@@ -185,6 +217,7 @@ protected void waitUntilContainerStarted() {
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
+ logger().info("Container is started (JDBC URL: {})", this.getJdbcUrl());
runInitScriptIfRequired();
}
@@ -239,9 +272,9 @@ public Connection createConnection(String queryString, Properties info)
SQLException lastException = null;
try {
- long start = System.currentTimeMillis();
+ long start = System.nanoTime();
// give up if we hit the time limit or the container stops running for some reason
- while (System.currentTimeMillis() < start + (1000 * connectTimeoutSeconds) && isRunning()) {
+ while ((System.nanoTime() - start < TimeUnit.SECONDS.toNanos(connectTimeoutSeconds)) && isRunning()) {
try {
logger()
.debug(
@@ -302,15 +335,25 @@ protected String constructUrlParameters(String startCharacter, String delimiter,
return urlParameters;
}
+ @Deprecated
protected void optionallyMapResourceParameterAsVolume(
@NotNull String paramName,
@NotNull String pathNameInContainer,
@NotNull String defaultResource
+ ) {
+ optionallyMapResourceParameterAsVolume(paramName, pathNameInContainer, defaultResource, null);
+ }
+
+ protected void optionallyMapResourceParameterAsVolume(
+ @NotNull String paramName,
+ @NotNull String pathNameInContainer,
+ @NotNull String defaultResource,
+ @Nullable Integer fileMode
) {
String resourceName = parameters.getOrDefault(paramName, defaultResource);
if (resourceName != null) {
- final MountableFile mountableFile = MountableFile.forClasspathResource(resourceName);
+ final MountableFile mountableFile = MountableFile.forClasspathResource(resourceName, fileMode);
withCopyFileToContainer(mountableFile, pathNameInContainer);
}
}
@@ -319,9 +362,10 @@ protected void optionallyMapResourceParameterAsVolume(
* Load init script content and apply it to the database if initScriptPath is set
*/
protected void runInitScriptIfRequired() {
- if (initScriptPath != null) {
- ScriptUtils.runInitScript(getDatabaseDelegate(), initScriptPath);
- }
+ initScriptPaths
+ .stream()
+ .filter(Objects::nonNull)
+ .forEach(path -> ScriptUtils.runInitScript(getDatabaseDelegate(), path));
}
public void setParameters(Map parameters) {
diff --git a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java
index b4aef740da0..ec606f3ed32 100644
--- a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java
+++ b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java
@@ -19,8 +19,6 @@
* This is an Immutable class holding JDBC Connection Url and its parsed components, used by {@link ContainerDatabaseDriver}.
*
* {@link ConnectionUrl#parseUrl()} method must be called after instantiating this class.
- *
- * @author manikmagar
*/
@EqualsAndHashCode(of = "url")
@Getter
@@ -109,7 +107,7 @@ private void parseUrl() {
//In case it matches to the default pattern
Matcher dbInstanceMatcher = Patterns.DB_INSTANCE_MATCHING_PATTERN.matcher(dbHostString);
if (dbInstanceMatcher.matches()) {
- databaseHost = Optional.of(dbInstanceMatcher.group("databaseHost"));
+ databaseHost = Optional.ofNullable(dbInstanceMatcher.group("databaseHost"));
databasePort = Optional.ofNullable(dbInstanceMatcher.group("databasePort")).map(Integer::valueOf);
databaseName = Optional.of(dbInstanceMatcher.group("databaseName"));
}
@@ -161,7 +159,7 @@ private Map parseTmpfsOptions(Map containerParam
}
/**
- * Get the TestContainers Parameters such as Init Function, Init Script path etc.
+ * Get the Testcontainers Parameters such as Init Function, Init Script path etc.
*
* @return {@link Map}
*/
@@ -179,7 +177,7 @@ private Map parseContainerParameters() {
}
/**
- * Get all Query parameters specified in the Connection URL after ?. This DOES NOT include TestContainers (TC_*) parameters.
+ * Get all Query parameters specified in the Connection URL after ?. This DOES NOT include Testcontainers (TC_*) parameters.
*
* @return {@link Map}
*/
@@ -203,8 +201,6 @@ public Map getTmpfsOptions() {
/**
* This interface defines the Regex Patterns used by {@link ConnectionUrl}.
- *
- * @author manikmagar
*/
public interface Patterns {
Pattern URL_MATCHING_PATTERN = Pattern.compile(
@@ -231,7 +227,7 @@ public interface Patterns {
//Matches to part of string - hostname:port/databasename
Pattern DB_INSTANCE_MATCHING_PATTERN = Pattern.compile(
- "(?[^:]+)" +
+ "(?[^:]+)?" +
"(:(?[0-9]+))?" +
"(" +
"(?[:/])" +
diff --git a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java
index a3fed34f24c..8f5f5f5a425 100644
--- a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java
+++ b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java
@@ -12,8 +12,18 @@
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
-import java.sql.*;
-import java.util.*;
+import java.sql.Connection;
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.DriverPropertyInfo;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ServiceLoader;
+import java.util.Set;
import java.util.logging.Logger;
import javax.script.ScriptException;
diff --git a/modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java b/modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java
index 7ed2f7ffde1..24c18944292 100644
--- a/modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java
+++ b/modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java
@@ -6,19 +6,20 @@
import org.testcontainers.exception.ConnectionCreationException;
import org.testcontainers.ext.ScriptUtils;
+import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
/**
* JDBC database delegate
- *
- * @author Eugeny Karpov
*/
@Slf4j
public class JdbcDatabaseDelegate extends AbstractDatabaseDelegate {
private JdbcDatabaseContainer container;
+ private Connection connection;
+
private String queryString;
public JdbcDatabaseDelegate(JdbcDatabaseContainer container, String queryString) {
@@ -29,7 +30,8 @@ public JdbcDatabaseDelegate(JdbcDatabaseContainer container, String queryString)
@Override
protected Statement createNewConnection() {
try {
- return container.createConnection(queryString).createStatement();
+ connection = container.createConnection(queryString);
+ return connection.createStatement();
} catch (SQLException e) {
log.error("Could not obtain JDBC connection");
throw new ConnectionCreationException("Could not obtain JDBC connection", e);
@@ -67,6 +69,7 @@ public void execute(
protected void closeConnectionQuietly(Statement statement) {
try {
statement.close();
+ connection.close();
} catch (Exception e) {
log.error("Could not close JDBC connection", e);
}
diff --git a/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java b/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java
index 1dc6646bd17..ca41c3f5d1c 100644
--- a/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java
+++ b/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java
@@ -1,7 +1,7 @@
package org.testcontainers.containers;
import lombok.NonNull;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import java.sql.Connection;
@@ -10,10 +10,10 @@
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.mock;
-public class JdbcDatabaseContainerTest {
+class JdbcDatabaseContainerTest {
@Test
- public void anExceptionIsThrownIfJdbcIsNotAvailable() {
+ void anExceptionIsThrownIfJdbcIsNotAvailable() {
JdbcDatabaseContainer> jdbcContainer = new JdbcDatabaseContainerStub("mysql:latest")
.withStartupTimeoutSeconds(1);
diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlDriversTests.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlDriversTests.java
index 2ac8cf88b5d..5797ee0083c 100644
--- a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlDriversTests.java
+++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlDriversTests.java
@@ -1,129 +1,126 @@
package org.testcontainers.jdbc;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameter;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
-import java.util.Arrays;
import java.util.Optional;
+import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
/**
* This Test class validates that all supported JDBC URL's can be parsed by ConnectionUrl class.
- *
- * @author ManikMagar
*/
-@RunWith(Parameterized.class)
-public class ConnectionUrlDriversTests {
+class ConnectionUrlDriversTests {
- @Parameter
- public String jdbcUrl;
-
- @Parameter(1)
- public String databaseType;
-
- @Parameter(2)
- public Optional tag;
-
- @Parameter(3)
- public String dbHostString;
-
- @Parameter(4)
- public String databaseName;
-
- @Parameterized.Parameters(name = "{index} - {0}")
- public static Iterable data() {
- return Arrays.asList(
- new Object[][] {
- { "jdbc:tc:mysql:5.7.34://hostname/test", "mysql", Optional.of("5.7.34"), "hostname/test", "test" },
- { "jdbc:tc:mysql://hostname/test", "mysql", Optional.empty(), "hostname/test", "test" },
- {
- "jdbc:tc:postgresql:1.2.3://hostname/test",
- "postgresql",
- Optional.of("1.2.3"),
- "hostname/test",
- "test",
- },
- { "jdbc:tc:postgresql://hostname/test", "postgresql", Optional.empty(), "hostname/test", "test" },
- {
- "jdbc:tc:sqlserver:1.2.3://localhost;instance=SQLEXPRESS:1433;databaseName=test",
- "sqlserver",
- Optional.of("1.2.3"),
- "localhost;instance=SQLEXPRESS:1433;databaseName=test",
- "test",
- },
- {
- "jdbc:tc:sqlserver://localhost;instance=SQLEXPRESS:1433;databaseName=test",
- "sqlserver",
- Optional.empty(),
- "localhost;instance=SQLEXPRESS:1433;databaseName=test",
- "test",
- },
- {
- "jdbc:tc:mariadb:1.2.3://localhost:3306/test",
- "mariadb",
- Optional.of("1.2.3"),
- "localhost:3306/test",
- "test",
- },
- { "jdbc:tc:mariadb://localhost:3306/test", "mariadb", Optional.empty(), "localhost:3306/test", "test" },
- {
- "jdbc:tc:oracle:1.2.3:thin://@localhost:1521/test",
- "oracle",
- Optional.of("1.2.3"),
- "localhost:1521/test",
- "test",
- },
- {
- "jdbc:tc:oracle:1.2.3:thin:@localhost:1521/test",
- "oracle",
- Optional.of("1.2.3"),
- "localhost:1521/test",
- "test",
- },
- {
- "jdbc:tc:oracle:thin:@localhost:1521/test",
- "oracle",
- Optional.empty(),
- "localhost:1521/test",
- "test",
- },
- {
- "jdbc:tc:oracle:1.2.3:thin:@localhost:1521:test",
- "oracle",
- Optional.of("1.2.3"),
- "localhost:1521:test",
- "test",
- },
- {
- "jdbc:tc:oracle:1.2.3:thin://@localhost:1521:test",
- "oracle",
- Optional.of("1.2.3"),
- "localhost:1521:test",
- "test",
- },
- {
- "jdbc:tc:oracle:1.2.3-anything:thin://@localhost:1521:test",
- "oracle",
- Optional.of("1.2.3-anything"),
- "localhost:1521:test",
- "test",
- },
- {
- "jdbc:tc:oracle:thin:@localhost:1521:test",
- "oracle",
- Optional.empty(),
- "localhost:1521:test",
- "test",
- },
- }
+ public static Stream data() {
+ return Stream.of(
+ Arguments.arguments(
+ "jdbc:tc:mysql:8.0.36://hostname/test",
+ "mysql",
+ Optional.of("8.0.36"),
+ "hostname/test",
+ "test"
+ ),
+ Arguments.arguments("jdbc:tc:mysql://hostname/test", "mysql", Optional.empty(), "hostname/test", "test"),
+ Arguments.arguments(
+ "jdbc:tc:postgresql:1.2.3://hostname/test",
+ "postgresql",
+ Optional.of("1.2.3"),
+ "hostname/test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:postgresql://hostname/test",
+ "postgresql",
+ Optional.empty(),
+ "hostname/test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:sqlserver:1.2.3://localhost;instance=SQLEXPRESS:1433;databaseName=test",
+ "sqlserver",
+ Optional.of("1.2.3"),
+ "localhost;instance=SQLEXPRESS:1433;databaseName=test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:sqlserver://localhost;instance=SQLEXPRESS:1433;databaseName=test",
+ "sqlserver",
+ Optional.empty(),
+ "localhost;instance=SQLEXPRESS:1433;databaseName=test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:mariadb:1.2.3://localhost:3306/test",
+ "mariadb",
+ Optional.of("1.2.3"),
+ "localhost:3306/test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:mariadb://localhost:3306/test",
+ "mariadb",
+ Optional.empty(),
+ "localhost:3306/test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:oracle:1.2.3:thin://@localhost:1521/test",
+ "oracle",
+ Optional.of("1.2.3"),
+ "localhost:1521/test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:oracle:1.2.3:thin:@localhost:1521/test",
+ "oracle",
+ Optional.of("1.2.3"),
+ "localhost:1521/test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:oracle:thin:@localhost:1521/test",
+ "oracle",
+ Optional.empty(),
+ "localhost:1521/test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:oracle:1.2.3:thin:@localhost:1521:test",
+ "oracle",
+ Optional.of("1.2.3"),
+ "localhost:1521:test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:oracle:1.2.3:thin://@localhost:1521:test",
+ "oracle",
+ Optional.of("1.2.3"),
+ "localhost:1521:test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:oracle:1.2.3-anything:thin://@localhost:1521:test",
+ "oracle",
+ Optional.of("1.2.3-anything"),
+ "localhost:1521:test",
+ "test"
+ ),
+ Arguments.arguments(
+ "jdbc:tc:oracle:thin:@localhost:1521:test",
+ "oracle",
+ Optional.empty(),
+ "localhost:1521:test",
+ "test"
+ )
);
}
- @Test
- public void test() {
+ @ParameterizedTest(name = "{index} - {0}")
+ @MethodSource("data")
+ void test(String jdbcUrl, String databaseType, Optional tag, String dbHostString, String databaseName) {
ConnectionUrl url = ConnectionUrl.newInstance(jdbcUrl);
assertThat(url.getDatabaseType()).as("Database Type is as expected").isEqualTo(databaseType);
assertThat(url.getImageTag()).as("Image tag is as expected").isEqualTo(tag);
diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java
index fa50dbbe76e..16abff74ec9 100644
--- a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java
+++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java
@@ -1,23 +1,19 @@
package org.testcontainers.jdbc;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
+import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
-public class ConnectionUrlTest {
-
- @Rule
- public ExpectedException thrown = ExpectedException.none();
+class ConnectionUrlTest {
@Test
- public void testConnectionUrl1() {
- String urlString = "jdbc:tc:mysql:5.7.34://somehostname:3306/databasename?a=b&c=d";
+ void testConnectionUrl1() {
+ String urlString = "jdbc:tc:mysql:8.0.36://somehostname:3306/databasename?a=b&c=d";
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
assertThat(url.getDatabaseType()).as("Database Type value is as expected").isEqualTo("mysql");
- assertThat(url.getImageTag()).as("Database Image tag value is as expected").contains("5.7.34");
+ assertThat(url.getImageTag()).as("Database Image tag value is as expected").contains("8.0.36");
assertThat(url.getDbHostString())
.as("Database Host String is as expected")
.isEqualTo("somehostname:3306/databasename");
@@ -31,7 +27,7 @@ public void testConnectionUrl1() {
}
@Test
- public void testConnectionUrl2() {
+ void testConnectionUrl2() {
String urlString = "jdbc:tc:mysql://somehostname/databasename";
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
@@ -49,13 +45,13 @@ public void testConnectionUrl2() {
}
@Test
- public void testEmptyQueryParameter() {
+ void testEmptyQueryParameter() {
ConnectionUrl url = ConnectionUrl.newInstance("jdbc:tc:mysql://somehostname/databasename?key=");
assertThat(url.getQueryParameters().get("key")).as("'key' property value").isEqualTo("");
}
@Test
- public void testTmpfsOption() {
+ void testTmpfsOption() {
String urlString = "jdbc:tc:mysql://somehostname/databasename?TC_TMPFS=key:value,key1:value1";
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
@@ -69,9 +65,9 @@ public void testTmpfsOption() {
}
@Test
- public void testInitScriptPathCapture() {
+ void testInitScriptPathCapture() {
String urlString =
- "jdbc:tc:mysql:5.7.34://somehostname:3306/databasename?a=b&c=d&TC_INITSCRIPT=somepath/init_mysql.sql";
+ "jdbc:tc:mysql:8.0.36://somehostname:3306/databasename?a=b&c=d&TC_INITSCRIPT=somepath/init_mysql.sql";
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
assertThat(url.getInitScriptPath())
@@ -83,15 +79,16 @@ public void testInitScriptPathCapture() {
.containsEntry("TC_INITSCRIPT", "somepath/init_mysql.sql");
//Parameter sets are unmodifiable
- thrown.expect(UnsupportedOperationException.class);
- url.getContainerParameters().remove("TC_INITSCRIPT");
- url.getQueryParameters().remove("a");
+ assertThatThrownBy(() -> url.getContainerParameters().remove("TC_INITSCRIPT"))
+ .isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(() -> url.getQueryParameters().remove("a"))
+ .isInstanceOf(UnsupportedOperationException.class);
}
@Test
- public void testInitFunctionCapture() {
+ void testInitFunctionCapture() {
String urlString =
- "jdbc:tc:mysql:5.7.34://somehostname:3306/databasename?a=b&c=d&TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction";
+ "jdbc:tc:mysql:8.0.36://somehostname:3306/databasename?a=b&c=d&TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction";
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
assertThat(url.getInitFunction()).as("Init Function parameter exists").isPresent();
@@ -105,10 +102,26 @@ public void testInitFunctionCapture() {
}
@Test
- public void testDaemonCapture() {
- String urlString = "jdbc:tc:mysql:5.7.34://somehostname:3306/databasename?a=b&c=d&TC_DAEMON=true";
+ void testDaemonCapture() {
+ String urlString = "jdbc:tc:mysql:8.0.36://somehostname:3306/databasename?a=b&c=d&TC_DAEMON=true";
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
assertThat(url.isInDaemonMode()).as("Daemon flag is set to true.").isTrue();
}
+
+ @Test
+ void testHostLessConnectionUrl() {
+ String urlString = "jdbc:tc:mysql:8.0.36:///databasename?a=b&c=d";
+ ConnectionUrl url = ConnectionUrl.newInstance(urlString);
+
+ assertThat(url.getDatabaseType()).as("Database Type value is as expected").isEqualTo("mysql");
+ assertThat(url.getImageTag()).as("Database Image tag value is as expected").contains("8.0.36");
+ assertThat(url.getQueryString()).as("Query String value is as expected").contains("?a=b&c=d");
+ assertThat(url.getDatabaseHost()).as("Database Host value is as expected").isEmpty();
+ assertThat(url.getDatabasePort()).as("Database Port value is as expected").isEmpty();
+ assertThat(url.getDatabaseName()).as("Database Name value is as expected").contains("databasename");
+
+ assertThat(url.getQueryParameters()).as("Parameter a is captured").containsEntry("a", "b");
+ assertThat(url.getQueryParameters()).as("Parameter c is captured").containsEntry("c", "d");
+ }
}
diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ContainerDatabaseDriverTest.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ContainerDatabaseDriverTest.java
index 4edd5d449ba..767258e19a1 100644
--- a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ContainerDatabaseDriverTest.java
+++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ContainerDatabaseDriverTest.java
@@ -1,9 +1,6 @@
package org.testcontainers.jdbc;
-import org.hamcrest.CoreMatchers;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
+import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;
@@ -11,25 +8,23 @@
import java.util.Properties;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
-public class ContainerDatabaseDriverTest {
+class ContainerDatabaseDriverTest {
private static final String PLAIN_POSTGRESQL_JDBC_URL = "jdbc:postgresql://localhost:5432/test";
- @Rule
- public ExpectedException thrown = ExpectedException.none();
-
@Test
- public void shouldNotTryToConnectToNonMatchingJdbcUrlDirectly() throws SQLException {
+ void shouldNotTryToConnectToNonMatchingJdbcUrlDirectly() throws SQLException {
ContainerDatabaseDriver driver = new ContainerDatabaseDriver();
Connection connection = driver.connect(PLAIN_POSTGRESQL_JDBC_URL, new Properties());
assertThat(connection).isNull();
}
@Test
- public void shouldNotTryToConnectToNonMatchingJdbcUrlViaDriverManager() throws SQLException {
- thrown.expect(SQLException.class);
- thrown.expectMessage(CoreMatchers.startsWith("No suitable driver found for "));
- DriverManager.getConnection(PLAIN_POSTGRESQL_JDBC_URL);
+ void shouldNotTryToConnectToNonMatchingJdbcUrlViaDriverManager() throws SQLException {
+ assertThatThrownBy(() -> DriverManager.getConnection(PLAIN_POSTGRESQL_JDBC_URL))
+ .isInstanceOf(SQLException.class)
+ .hasMessageStartingWith("No suitable driver found for ");
}
}
diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/JdbcDatabaseDelegateTest.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/JdbcDatabaseDelegateTest.java
new file mode 100644
index 00000000000..e6233e14b9f
--- /dev/null
+++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/JdbcDatabaseDelegateTest.java
@@ -0,0 +1,87 @@
+package org.testcontainers.jdbc;
+
+import lombok.NonNull;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class JdbcDatabaseDelegateTest {
+
+ @Test
+ void testLeakedConnections() {
+ final JdbcDatabaseContainerStub stub = new JdbcDatabaseContainerStub(DockerImageName.parse("something"));
+ try (JdbcDatabaseDelegate delegate = new JdbcDatabaseDelegate(stub, "")) {
+ delegate.execute("foo", null, 0, false, false);
+ }
+ assertThat(stub.openConnectionsList.size()).isZero();
+ }
+
+ static class JdbcDatabaseContainerStub extends JdbcDatabaseContainer {
+
+ List openConnectionsList = new ArrayList<>();
+
+ public JdbcDatabaseContainerStub(@NonNull DockerImageName dockerImageName) {
+ super(dockerImageName);
+ }
+
+ @Override
+ public String getDriverClassName() {
+ return null;
+ }
+
+ @Override
+ public String getJdbcUrl() {
+ return null;
+ }
+
+ @Override
+ public String getUsername() {
+ return null;
+ }
+
+ @Override
+ public String getPassword() {
+ return null;
+ }
+
+ @Override
+ protected String getTestQueryString() {
+ return null;
+ }
+
+ @Override
+ public boolean isRunning() {
+ return true;
+ }
+
+ @Override
+ public Connection createConnection(String queryString) throws NoDriverFoundException, SQLException {
+ final Connection connection = mock(Connection.class);
+ openConnectionsList.add(connection);
+ when(connection.createStatement()).thenReturn(mock(Statement.class));
+ connection.close();
+ Mockito.doAnswer(ignore -> openConnectionsList.remove(connection)).when(connection).close();
+ return connection;
+ }
+
+ @Override
+ protected Logger logger() {
+ return mock(Logger.class);
+ }
+
+ @Override
+ public void setDockerImageName(@NonNull String dockerImageName) {}
+ }
+}
diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java
index 379b262aa3a..469234e15a6 100644
--- a/modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java
+++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java
@@ -1,7 +1,7 @@
package org.testcontainers.jdbc;
import com.google.common.base.Throwables;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.utility.DockerImageName;
@@ -12,10 +12,10 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
-public class MissingJdbcDriverTest {
+class MissingJdbcDriverTest {
@Test
- public void shouldFailFastIfNoDriverFound() {
+ void shouldFailFastIfNoDriverFound() {
final MissingDriverContainer container = new MissingDriverContainer();
try {
@@ -43,7 +43,7 @@ static class MissingDriverContainer extends JdbcDatabaseContainer {
private final AtomicInteger connectionAttempts = new AtomicInteger();
MissingDriverContainer() {
- super(DockerImageName.parse("mysql:5.7.34"));
+ super(DockerImageName.parse("mysql:8.0.36"));
withEnv("MYSQL_ROOT_PASSWORD", "test");
withExposedPorts(3306);
}
diff --git a/modules/jdbc/src/test/resources/logback-test.xml b/modules/jdbc/src/test/resources/logback-test.xml
index 535e406fc13..83ef7a1a3ef 100644
--- a/modules/jdbc/src/test/resources/logback-test.xml
+++ b/modules/jdbc/src/test/resources/logback-test.xml
@@ -12,5 +12,5 @@
-
+
diff --git a/modules/junit-jupiter/build.gradle b/modules/junit-jupiter/build.gradle
index 4987c027a37..b9ac700cea4 100644
--- a/modules/junit-jupiter/build.gradle
+++ b/modules/junit-jupiter/build.gradle
@@ -2,27 +2,18 @@ description = "Testcontainers :: JUnit Jupiter Extension"
dependencies {
api project(':testcontainers')
- api 'org.junit.jupiter:junit-jupiter-api:5.9.1'
+ implementation platform('org.junit:junit-bom:5.14.3')
+ implementation 'org.junit.jupiter:junit-jupiter-api'
- testImplementation project(':mysql')
- testImplementation project(':postgresql')
- testImplementation 'com.zaxxer:HikariCP:4.0.3'
- testImplementation 'redis.clients:jedis:4.3.1'
- testImplementation 'org.apache.httpcomponents:httpclient:4.5.13'
- testImplementation ('org.mockito:mockito-core:4.8.1') {
+ testImplementation project(':testcontainers-mysql')
+ testImplementation project(':testcontainers-postgresql')
+ testImplementation 'com.zaxxer:HikariCP:7.0.2'
+ testImplementation 'redis.clients:jedis:7.5.3'
+ testImplementation 'org.apache.httpcomponents:httpclient:4.5.14'
+ testImplementation ('org.mockito:mockito-core:4.11.0') {
exclude(module: 'hamcrest-core')
}
- testImplementation 'org.assertj:assertj-core:3.23.1'
- testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.1'
- testRuntimeOnly 'org.postgresql:postgresql:42.5.0'
- testRuntimeOnly 'mysql:mysql-connector-java:8.0.31'
- testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
-}
-
-test {
- useJUnitPlatform()
- testLogging {
- events "passed", "skipped", "failed"
- }
+ testRuntimeOnly 'org.postgresql:postgresql:42.7.12'
+ testRuntimeOnly 'com.mysql:mysql-connector-j:9.6.0'
}
diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/Container.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/Container.java
index a075786d0d5..a22c23e57a6 100644
--- a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/Container.java
+++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/Container.java
@@ -11,7 +11,7 @@
*
* @see Testcontainers
*/
-@Target(ElementType.FIELD)
+@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Container {
}
diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/DockerAvailableDetector.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/DockerAvailableDetector.java
new file mode 100644
index 00000000000..804568e7dfc
--- /dev/null
+++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/DockerAvailableDetector.java
@@ -0,0 +1,15 @@
+package org.testcontainers.junit.jupiter;
+
+import org.testcontainers.DockerClientFactory;
+
+class DockerAvailableDetector {
+
+ public boolean isDockerAvailable() {
+ try {
+ DockerClientFactory.instance().client();
+ return true;
+ } catch (Throwable ex) {
+ return false;
+ }
+ }
+}
diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailable.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailable.java
new file mode 100644
index 00000000000..6c78cbb7759
--- /dev/null
+++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailable.java
@@ -0,0 +1,19 @@
+package org.testcontainers.junit.jupiter;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * {@code EnabledIfDockerAvailable} is a JUnit Jupiter extension to enable tests only if Docker is available.
+ */
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@ExtendWith(EnabledIfDockerAvailableCondition.class)
+public @interface EnabledIfDockerAvailable {
+}
diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableCondition.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableCondition.java
new file mode 100644
index 00000000000..6067a9b4f4e
--- /dev/null
+++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableCondition.java
@@ -0,0 +1,47 @@
+package org.testcontainers.junit.jupiter;
+
+import org.junit.jupiter.api.extension.ConditionEvaluationResult;
+import org.junit.jupiter.api.extension.ExecutionCondition;
+import org.junit.jupiter.api.extension.ExtensionConfigurationException;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.platform.commons.support.AnnotationSupport;
+
+import java.util.Optional;
+
+class EnabledIfDockerAvailableCondition implements ExecutionCondition {
+
+ private final DockerAvailableDetector dockerDetector = new DockerAvailableDetector();
+
+ @Override
+ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
+ return findAnnotation(context)
+ .map(this::evaluate)
+ .orElseThrow(() -> new ExtensionConfigurationException("@EnabledIfDockerAvailable not found"));
+ }
+
+ boolean isDockerAvailable() {
+ return this.dockerDetector.isDockerAvailable();
+ }
+
+ private ConditionEvaluationResult evaluate(EnabledIfDockerAvailable testcontainers) {
+ if (isDockerAvailable()) {
+ return ConditionEvaluationResult.enabled("Docker is available");
+ }
+ return ConditionEvaluationResult.disabled("Docker is not available");
+ }
+
+ private Optional findAnnotation(ExtensionContext context) {
+ Optional current = Optional.of(context);
+ while (current.isPresent()) {
+ Optional enabledIfDockerAvailable = AnnotationSupport.findAnnotation(
+ current.get().getRequiredTestClass(),
+ EnabledIfDockerAvailable.class
+ );
+ if (enabledIfDockerAvailable.isPresent()) {
+ return enabledIfDockerAvailable;
+ }
+ current = current.get().getParent();
+ }
+ return Optional.empty();
+ }
+}
diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/Testcontainers.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/Testcontainers.java
index 730339f0fd5..8f9915ab334 100644
--- a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/Testcontainers.java
+++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/Testcontainers.java
@@ -12,7 +12,7 @@
* {@code @Testcontainers} is a JUnit Jupiter extension to activate automatic
* startup and stop of containers used in a test case.
*
- * The test containers extension finds all fields that are annotated with
+ *
The Testcontainers extension finds all fields that are annotated with
* {@link Container} and calls their container lifecycle methods. Containers
* declared as static fields will be shared between test methods. They will be
* started only once before any test method is executed and stopped after the
@@ -60,7 +60,15 @@
@Inherited
public @interface Testcontainers {
/**
- * Whether tests should be disabled (rather than failing) when Docker is not available.
+ * Whether tests should be disabled (rather than failing) when Docker is not available. Defaults to
+ * {@code false}.
+ * @return if the tests should be disabled when Docker is not available
*/
boolean disabledWithoutDocker() default false;
+
+ /**
+ * Whether containers should start in parallel. Defaults to {@code false}.
+ * @return if the containers should start in parallel
+ */
+ boolean parallel() default false;
}
diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/TestcontainersExtension.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/TestcontainersExtension.java
index 757083dc322..89adba6033f 100644
--- a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/TestcontainersExtension.java
+++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/TestcontainersExtension.java
@@ -16,8 +16,8 @@
import org.junit.platform.commons.support.HierarchyTraversalMode;
import org.junit.platform.commons.support.ModifierSupport;
import org.junit.platform.commons.support.ReflectionSupport;
-import org.testcontainers.DockerClientFactory;
import org.testcontainers.lifecycle.Startable;
+import org.testcontainers.lifecycle.Startables;
import org.testcontainers.lifecycle.TestDescription;
import org.testcontainers.lifecycle.TestLifecycleAware;
@@ -41,6 +41,8 @@ public class TestcontainersExtension
private static final String LOCAL_LIFECYCLE_AWARE_CONTAINERS = "localLifecycleAwareContainers";
+ private final DockerAvailableDetector dockerDetector = new DockerAvailableDetector();
+
@Override
public void beforeAll(ExtensionContext context) {
Class> testClass = context
@@ -52,9 +54,7 @@ public void beforeAll(ExtensionContext context) {
Store store = context.getStore(NAMESPACE);
List sharedContainersStoreAdapters = findSharedContainers(testClass);
- sharedContainersStoreAdapters.forEach(adapter -> {
- store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start());
- });
+ startContainers(sharedContainersStoreAdapters, store, context);
List lifecycleAwareContainers = sharedContainersStoreAdapters
.stream()
@@ -66,6 +66,24 @@ public void beforeAll(ExtensionContext context) {
signalBeforeTestToContainers(lifecycleAwareContainers, testDescriptionFrom(context));
}
+ private void startContainers(List storeAdapters, Store store, ExtensionContext context) {
+ if (storeAdapters.isEmpty()) {
+ return;
+ }
+
+ if (isParallelExecutionEnabled(context)) {
+ Stream startables = storeAdapters
+ .stream()
+ .map(storeAdapter -> {
+ store.getOrComputeIfAbsent(storeAdapter.getKey(), k -> storeAdapter);
+ return storeAdapter.container;
+ });
+ Startables.deepStart(startables).join();
+ } else {
+ storeAdapters.forEach(adapter -> store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()));
+ }
+ }
+
@Override
public void afterAll(ExtensionContext context) {
signalAfterTestToContainersFor(SHARED_LIFECYCLE_AWARE_CONTAINERS, context);
@@ -75,18 +93,39 @@ public void afterAll(ExtensionContext context) {
public void beforeEach(final ExtensionContext context) {
Store store = context.getStore(NAMESPACE);
- List lifecycleAwareContainers = collectParentTestInstances(context)
+ List restartContainers = collectParentTestInstances(context)
.parallelStream()
.flatMap(this::findRestartContainers)
- .peek(adapter -> store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()))
- .filter(this::isTestLifecycleAware)
- .map(lifecycleAwareAdapter -> (TestLifecycleAware) lifecycleAwareAdapter.container)
.collect(Collectors.toList());
+ List lifecycleAwareContainers = findTestLifecycleAwareContainers(
+ restartContainers,
+ store,
+ context
+ );
+
store.put(LOCAL_LIFECYCLE_AWARE_CONTAINERS, lifecycleAwareContainers);
signalBeforeTestToContainers(lifecycleAwareContainers, testDescriptionFrom(context));
}
+ private List findTestLifecycleAwareContainers(
+ List restartContainers,
+ Store store,
+ ExtensionContext context
+ ) {
+ startContainers(restartContainers, store, context);
+
+ return restartContainers
+ .stream()
+ .filter(this::isTestLifecycleAware)
+ .map(lifecycleAwareAdapter -> (TestLifecycleAware) lifecycleAwareAdapter.container)
+ .collect(Collectors.toList());
+ }
+
+ private boolean isParallelExecutionEnabled(ExtensionContext context) {
+ return findTestcontainers(context).map(Testcontainers::parallel).orElse(false);
+ }
+
@Override
public void afterEach(ExtensionContext context) {
signalAfterTestToContainersFor(LOCAL_LIFECYCLE_AWARE_CONTAINERS, context);
@@ -154,12 +193,7 @@ private ConditionEvaluationResult evaluate(Testcontainers testcontainers) {
}
boolean isDockerAvailable() {
- try {
- DockerClientFactory.instance().client();
- return true;
- } catch (Throwable ex) {
- return false;
- }
+ return this.dockerDetector.isDockerAvailable();
}
private Set collectParentTestInstances(final ExtensionContext context) {
@@ -226,7 +260,7 @@ private static StoreAdapter getContainerInstance(final Object testInstance, fina
* thereby letting the JUnit automatically stop containers once the current
* {@link ExtensionContext} is closed.
*/
- private static class StoreAdapter implements CloseableResource {
+ private static class StoreAdapter implements CloseableResource, AutoCloseable {
@Getter
private String key;
diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ComposeContainerTests.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ComposeContainerTests.java
index 866b76df301..a60b24a23d2 100644
--- a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ComposeContainerTests.java
+++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ComposeContainerTests.java
@@ -4,9 +4,8 @@
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.testcontainers.containers.DockerComposeContainer;
+import org.testcontainers.containers.ComposeContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import java.io.File;
@@ -17,25 +16,16 @@
class ComposeContainerTests {
@Container
- private DockerComposeContainer composeContainer = new DockerComposeContainer(
- new File("src/test/resources/docker-compose.yml")
- )
- .withExposedService("whoami_1", 80, Wait.forHttp("/"));
-
- private String host;
-
- private int port;
-
- @BeforeEach
- void setup() {
- host = composeContainer.getServiceHost("whoami_1", 80);
- port = composeContainer.getServicePort("whoami_1", 80);
- }
+ private ComposeContainer composeContainer = new ComposeContainer(new File("src/test/resources/docker-compose.yml"))
+ .withExposedService("whoami-1", 80, Wait.forHttp("/"));
@Test
void running_compose_defined_container_is_accessible_on_configured_port() throws Exception {
HttpClient client = HttpClientBuilder.create().build();
+ String host = composeContainer.getServiceHost("whoami-1", 80);
+ int port = composeContainer.getServicePort("whoami-1", 80);
+
HttpResponse response = client.execute(new HttpGet("http://" + host + ":" + port));
assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/DockerComposeContainerTests.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/DockerComposeContainerTests.java
new file mode 100644
index 00000000000..460d50a856b
--- /dev/null
+++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/DockerComposeContainerTests.java
@@ -0,0 +1,37 @@
+package org.testcontainers.junit.jupiter;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.DockerComposeContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@Testcontainers
+class DockerComposeContainerTests {
+
+ @Container
+ private DockerComposeContainer composeContainer = new DockerComposeContainer(
+ DockerImageName.parse("docker/compose:1.29.2"),
+ new File("src/test/resources/docker-compose.yml")
+ )
+ .withExposedService("whoami_1", 80, Wait.forHttp("/"));
+
+ @Test
+ void running_compose_defined_container_is_accessible_on_configured_port() throws Exception {
+ HttpClient client = HttpClientBuilder.create().build();
+
+ String host = composeContainer.getServiceHost("whoami_1", 80);
+ int port = composeContainer.getServicePort("whoami_1", 80);
+
+ HttpResponse response = client.execute(new HttpGet("http://" + host + ":" + port));
+
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
+ }
+}
diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableTests.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableTests.java
new file mode 100644
index 00000000000..455e94788f9
--- /dev/null
+++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableTests.java
@@ -0,0 +1,48 @@
+package org.testcontainers.junit.jupiter;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ConditionEvaluationResult;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class EnabledIfDockerAvailableTests {
+
+ @Test
+ void whenDockerIsAvailableTestsAreEnabled() {
+ ConditionEvaluationResult result = new TestEnabledIfDockerAvailableCondition(true)
+ .evaluateExecutionCondition(extensionContext(DisabledWithoutDocker.class));
+ assertThat(result.isDisabled()).isFalse();
+ }
+
+ @Test
+ void whenDockerIsUnavailableTestsAreDisabled() {
+ ConditionEvaluationResult result = new TestEnabledIfDockerAvailableCondition(false)
+ .evaluateExecutionCondition(extensionContext(DisabledWithoutDocker.class));
+ assertThat(result.isDisabled()).isTrue();
+ }
+
+ private ExtensionContext extensionContext(Class clazz) {
+ ExtensionContext extensionContext = mock(ExtensionContext.class);
+ when(extensionContext.getRequiredTestClass()).thenReturn(clazz);
+ return extensionContext;
+ }
+
+ @EnabledIfDockerAvailable
+ static final class DisabledWithoutDocker {}
+
+ static final class TestEnabledIfDockerAvailableCondition extends EnabledIfDockerAvailableCondition {
+
+ private final boolean dockerAvailable;
+
+ private TestEnabledIfDockerAvailableCondition(boolean dockerAvailable) {
+ this.dockerAvailable = dockerAvailable;
+ }
+
+ boolean isDockerAvailable() {
+ return dockerAvailable;
+ }
+ }
+}
diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java
index e99d1ec720c..4343b5dffc8 100644
--- a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java
+++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java
@@ -4,5 +4,8 @@
public interface JUnitJupiterTestImages {
DockerImageName POSTGRES_IMAGE = DockerImageName.parse("postgres:9.6.12");
+
DockerImageName HTTPD_IMAGE = DockerImageName.parse("httpd:2.4-alpine");
+
+ DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8.0.32");
}
diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MetaAnnotationTest.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MetaAnnotationTest.java
new file mode 100644
index 00000000000..627d778a93f
--- /dev/null
+++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MetaAnnotationTest.java
@@ -0,0 +1,28 @@
+package org.testcontainers.junit.jupiter;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.PostgreSQLContainer;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@Testcontainers
+class MetaAnnotationTest {
+
+ @TcContainer
+ private static final PostgreSQLContainer> POSTGRESQL = new PostgreSQLContainer<>(
+ JUnitJupiterTestImages.POSTGRES_IMAGE
+ );
+
+ @Test
+ void test() {
+ assertThat(POSTGRESQL.isRunning()).isTrue();
+ }
+}
+
+@Container
+@Retention(RetentionPolicy.RUNTIME)
+@interface TcContainer {
+}
diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java
index e8ce714ab28..7dcd769bf08 100644
--- a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java
+++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java
@@ -12,11 +12,11 @@ class MixedLifecycleTests {
// will be shared between test methods
@Container
- private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer();
+ private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer("mysql:8.0.36");
// will be started before and stopped after each test method
@Container
- private PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer()
+ private PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer("postgres:9.6.12")
.withDatabaseName("foo")
.withUsername("foo")
.withPassword("secret");
diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ParallelExecutionTests.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ParallelExecutionTests.java
new file mode 100644
index 00000000000..84d11a8235b
--- /dev/null
+++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ParallelExecutionTests.java
@@ -0,0 +1,28 @@
+package org.testcontainers.junit.jupiter;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.containers.PostgreSQLContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@Testcontainers(parallel = true)
+public class ParallelExecutionTests {
+
+ @Container
+ private static final PostgreSQLContainer> POSTGRESQL_CONTAINER = new PostgreSQLContainer<>(
+ JUnitJupiterTestImages.POSTGRES_IMAGE
+ )
+ .withDatabaseName("foo")
+ .withUsername("foo")
+ .withPassword("secret");
+
+ @Container
+ private MySQLContainer> mySQLContainer = new MySQLContainer<>(JUnitJupiterTestImages.MYSQL_IMAGE);
+
+ @Test
+ void test() {
+ assertThat(POSTGRESQL_CONTAINER.isRunning()).isTrue();
+ assertThat(mySQLContainer.isRunning()).isTrue();
+ }
+}
diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestLifecycleAwareExceptionCapturingTest.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestLifecycleAwareExceptionCapturingTest.java
index b64f2f3c328..a6647aa3e52 100644
--- a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestLifecycleAwareExceptionCapturingTest.java
+++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestLifecycleAwareExceptionCapturingTest.java
@@ -1,13 +1,13 @@
package org.testcontainers.junit.jupiter;
-import org.junit.AssumptionViolatedException;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
+import org.opentest4j.TestAbortedException;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assume.assumeTrue;
+import static org.assertj.core.api.Assumptions.assumeThat;
// The order of @ExtendsWith and @Testcontainers is crucial in order for the tests
@Testcontainers
@@ -24,14 +24,14 @@ class TestLifecycleAwareExceptionCapturingTest {
void failing_test_should_pass_throwable_to_testContainer() {
startedTestContainer = testContainer;
// Force an exception that is captured by the test container without failing the test itself
- assumeTrue(false);
+ assumeThat(false).isTrue();
}
@Test
@Order(2)
void should_have_captured_thrownException() {
Throwable capturedThrowable = startedTestContainer.getCapturedThrowable();
- assertThat(capturedThrowable).isInstanceOf(AssumptionViolatedException.class);
- assertThat(capturedThrowable.getMessage()).isEqualTo("got: , expected: is ");
+ assertThat(capturedThrowable).isInstanceOf(TestAbortedException.class);
+ assertThat(capturedThrowable.getMessage()).contains("Expecting value to be true but was false");
}
}
diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/inheritance/RedisContainer.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/inheritance/RedisContainer.java
index 0a9ec62fca2..f6ed39c483e 100644
--- a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/inheritance/RedisContainer.java
+++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/inheritance/RedisContainer.java
@@ -7,7 +7,7 @@
public class RedisContainer extends GenericContainer {
public RedisContainer() {
- super(DockerImageName.parse("redis:3.2.11"));
+ super(DockerImageName.parse("redis:6-alpine"));
withExposedPorts(6379);
}
diff --git a/modules/junit-jupiter/src/test/resources/logback-test.xml b/modules/junit-jupiter/src/test/resources/logback-test.xml
index 535e406fc13..83ef7a1a3ef 100644
--- a/modules/junit-jupiter/src/test/resources/logback-test.xml
+++ b/modules/junit-jupiter/src/test/resources/logback-test.xml
@@ -12,5 +12,5 @@
-
+
diff --git a/modules/k3s/build.gradle b/modules/k3s/build.gradle
index 81c4fc253c0..0a8d628b469 100644
--- a/modules/k3s/build.gradle
+++ b/modules/k3s/build.gradle
@@ -3,12 +3,9 @@ description = "Testcontainers :: K3S"
dependencies {
api project(":testcontainers")
- // https://youtu.be/otCpCn0l4Wo
- // The core module depends on jackson-databind 2.8.x for backward compatibility.
- // Any >2.8 version here is not compatible with jackson-databind 2.8.x.
- shaded 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.8.8'
+ // Synchronize with the jackson version, must match major and minor version
+ shaded 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.4'
- testImplementation 'io.fabric8:kubernetes-client:6.2.0'
- testImplementation 'io.kubernetes:client-java:16.0.1'
- testImplementation 'org.assertj:assertj-core:3.23.1'
+ testImplementation 'io.fabric8:kubernetes-client:7.8.0'
+ testImplementation 'io.kubernetes:client-java:25.0.0-legacy'
}
diff --git a/modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java b/modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java
index c9dad93a06e..6dd6f06d85e 100644
--- a/modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java
+++ b/modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java
@@ -10,13 +10,18 @@
import org.apache.commons.io.IOUtils;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
+/**
+ * Testcontainers implementation for K3S
+ *
+ * Supported image: {@code rancher/k3s}
+ */
public class K3sContainer extends GenericContainer {
public static int KUBE_SECURE_PORT = 6443;
@@ -41,8 +46,8 @@ public K3sContainer(DockerImageName dockerImageName) {
tmpFsMapping.put("/var/run", "");
setTmpFsMapping(tmpFsMapping);
- setCommand("server", "--no-deploy=traefik", "--tls-san=" + this.getHost());
- setWaitStrategy(new LogMessageWaitStrategy().withRegEx(".*Node controller sync successful.*"));
+ setCommand("server", "--disable=traefik", "--tls-san=" + this.getHost());
+ setWaitStrategy(Wait.forLogMessage(".*Node controller sync successful.*", 1));
}
@Override
diff --git a/modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java b/modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java
index 42b08444555..50b2e17f1fc 100644
--- a/modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java
+++ b/modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java
@@ -12,7 +12,7 @@
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.dsl.Resource;
import lombok.extern.slf4j.Slf4j;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.utility.DockerImageName;
@@ -22,10 +22,10 @@
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
-public class Fabric8K3sContainerTest {
+class Fabric8K3sContainerTest {
@Test
- public void shouldStartAndHaveListableNode() {
+ void shouldStartAndHaveListableNode() {
try (
// starting_k3s {
K3sContainer k3s = new K3sContainer(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1"))
diff --git a/modules/k3s/src/test/java/org/testcontainers/k3s/KubectlContainerTest.java b/modules/k3s/src/test/java/org/testcontainers/k3s/KubectlContainerTest.java
index 5c0d44ff0f1..6dcd7421363 100644
--- a/modules/k3s/src/test/java/org/testcontainers/k3s/KubectlContainerTest.java
+++ b/modules/k3s/src/test/java/org/testcontainers/k3s/KubectlContainerTest.java
@@ -1,7 +1,8 @@
package org.testcontainers.k3s;
-import org.junit.ClassRule;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
@@ -11,16 +12,26 @@
import java.time.Duration;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
-public class KubectlContainerTest {
+class KubectlContainerTest {
- public static Network network = Network.SHARED;
+ private static final Network network = Network.SHARED;
- @ClassRule
- public static K3sContainer k3s = new K3sContainer(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1"))
+ private static final K3sContainer k3s = new K3sContainer(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1"))
.withNetwork(network)
.withNetworkAliases("k3s");
+ @BeforeAll
+ static void setup() {
+ k3s.start();
+ }
+
+ @AfterAll
+ static void teardown() {
+ k3s.stop();
+ }
+
@Test
public void shouldExposeKubeConfigForNetworkAlias() throws Exception {
String kubeConfigYaml = k3s.generateInternalKubeConfigYaml("k3s");
@@ -38,8 +49,9 @@ public void shouldExposeKubeConfigForNetworkAlias() throws Exception {
}
}
- @Test(expected = IllegalArgumentException.class)
+ @Test
public void shouldThrowAnExceptionForUnknownNetworkAlias() {
- k3s.generateInternalKubeConfigYaml("not-set-network-alias");
+ assertThatThrownBy(() -> k3s.generateInternalKubeConfigYaml("not-set-network-alias"))
+ .isInstanceOf(IllegalArgumentException.class);
}
}
diff --git a/modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java b/modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java
index c2504c27dbe..f71cbae558b 100644
--- a/modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java
+++ b/modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java
@@ -6,7 +6,7 @@
import io.kubernetes.client.openapi.models.V1NodeList;
import io.kubernetes.client.util.Config;
import lombok.extern.slf4j.Slf4j;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.utility.DockerImageName;
@@ -16,16 +16,20 @@
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
-public class OfficialClientK3sContainerTest {
+class OfficialClientK3sContainerTest {
@Test
- public void shouldStartAndHaveListableNode() throws IOException, ApiException {
- try (
- // starting_k3s {
- K3sContainer k3s = new K3sContainer(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1"))
- .withLogConsumer(new Slf4jLogConsumer(log))
- // }
- ) {
+ void shouldStartAndHaveListableNode() throws IOException, ApiException {
+ runK3s(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1"));
+ }
+
+ @Test
+ void shouldStartAndHaveListableNodeUsingLowerVersion() throws IOException, ApiException {
+ runK3s(DockerImageName.parse("rancher/k3s:v1.20.15-k3s1"));
+ }
+
+ private void runK3s(DockerImageName k3sDockerImage) throws IOException, ApiException {
+ try (K3sContainer k3s = new K3sContainer(k3sDockerImage).withLogConsumer(new Slf4jLogConsumer(log))) {
k3s.start();
// connecting_with_k8sio {
@@ -35,7 +39,7 @@ public void shouldStartAndHaveListableNode() throws IOException, ApiException {
CoreV1Api api = new CoreV1Api(client);
// interact with the running K3s server, e.g.:
- V1NodeList nodes = api.listNode(null, null, null, null, null, null, null, null, null, null);
+ V1NodeList nodes = api.listNode(null, null, null, null, null, null, null, null, null, null, null);
// }
assertThat(nodes.getItems()).hasSize(1);
diff --git a/modules/k3s/src/test/resources/logback-test.xml b/modules/k3s/src/test/resources/logback-test.xml
index 535e406fc13..83ef7a1a3ef 100644
--- a/modules/k3s/src/test/resources/logback-test.xml
+++ b/modules/k3s/src/test/resources/logback-test.xml
@@ -12,5 +12,5 @@
-
+
diff --git a/modules/k6/build.gradle b/modules/k6/build.gradle
new file mode 100644
index 00000000000..0ca874ddc25
--- /dev/null
+++ b/modules/k6/build.gradle
@@ -0,0 +1,5 @@
+description = "Testcontainers :: k6"
+
+dependencies {
+ api project(':testcontainers')
+}
diff --git a/modules/k6/src/main/java/org/testcontainers/k6/K6Container.java b/modules/k6/src/main/java/org/testcontainers/k6/K6Container.java
new file mode 100644
index 00000000000..04403ccb4cb
--- /dev/null
+++ b/modules/k6/src/main/java/org/testcontainers/k6/K6Container.java
@@ -0,0 +1,88 @@
+package org.testcontainers.k6;
+
+import org.apache.commons.io.FilenameUtils;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class K6Container extends GenericContainer {
+
+ /** Standard image for k6, as provided by Grafana. */
+ private static final DockerImageName K6_IMAGE = DockerImageName.parse("grafana/k6");
+
+ private String testScript;
+
+ private List cmdOptions = new ArrayList<>();
+
+ private Map scriptVars = new HashMap<>();
+
+ /**
+ * Creates a new container instance based upon the provided image name.
+ */
+ public K6Container(String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ /**
+ * Creates a new container instance based upon the provided image.
+ */
+ public K6Container(DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(K6_IMAGE);
+ }
+
+ /**
+ * Specifies the test script to be executed within the container.
+ * @param testScript file to be copied into the container
+ * @return the builder
+ */
+ public K6Container withTestScript(MountableFile testScript) {
+ this.testScript = "/home/k6/" + FilenameUtils.getName(testScript.getResolvedPath());
+ withCopyFileToContainer(testScript, this.testScript);
+ return self();
+ }
+
+ /**
+ * Specifies additional command line options to be provided to the k6 command.
+ * @param options command line options
+ * @return the builder
+ */
+ public K6Container withCmdOptions(String... options) {
+ cmdOptions.addAll(Arrays.asList(options));
+ return self();
+ }
+
+ /**
+ * Adds a key-value pair for access within test scripts as an environment variable.
+ * @param key unique identifier for the variable
+ * @param value value of the variable
+ * @return the builder
+ */
+ public K6Container withScriptVar(String key, String value) {
+ scriptVars.put(key, value);
+ return self();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void configure() {
+ List commandParts = new ArrayList<>();
+ commandParts.add("run");
+ commandParts.addAll(cmdOptions);
+ for (Map.Entry entry : scriptVars.entrySet()) {
+ commandParts.add("--env");
+ commandParts.add(String.format("%s=%s", entry.getKey(), entry.getValue()));
+ }
+ commandParts.add(testScript);
+
+ setCommand(commandParts.toArray(new String[] {}));
+ }
+}
diff --git a/modules/k6/src/test/java/org/testcontainers/k6/K6ContainerTests.java b/modules/k6/src/test/java/org/testcontainers/k6/K6ContainerTests.java
new file mode 100644
index 00000000000..43b390c4db4
--- /dev/null
+++ b/modules/k6/src/test/java/org/testcontainers/k6/K6ContainerTests.java
@@ -0,0 +1,43 @@
+package org.testcontainers.k6;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.output.WaitingConsumer;
+import org.testcontainers.utility.MountableFile;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class K6ContainerTests {
+
+ @Test
+ void k6StandardTest() throws Exception {
+ try (
+ // standard_k6 {
+ K6Container container = new K6Container("grafana/k6:0.49.0")
+ .withTestScript(MountableFile.forClasspathResource("scripts/test.js"))
+ .withScriptVar("MY_SCRIPT_VAR", "are cool!")
+ .withScriptVar("AN_UNUSED_VAR", "unused")
+ .withCmdOptions("--quiet", "--no-usage-report")
+ // }
+ ) {
+ container.start();
+
+ // wait {
+ WaitingConsumer consumer = new WaitingConsumer();
+ container.followOutput(consumer);
+
+ // Wait for test script results to be collected
+ consumer.waitUntil(
+ frame -> {
+ return frame.getUtf8String().contains("iteration_duration");
+ },
+ 3,
+ TimeUnit.SECONDS
+ );
+ // }
+
+ assertThat(container.getLogs()).contains("k6 tests are cool!");
+ }
+ }
+}
diff --git a/modules/k6/src/test/resources/logback-test.xml b/modules/k6/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..83ef7a1a3ef
--- /dev/null
+++ b/modules/k6/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/modules/k6/src/test/resources/scripts/test.js b/modules/k6/src/test/resources/scripts/test.js
new file mode 100644
index 00000000000..fad0c4b48df
--- /dev/null
+++ b/modules/k6/src/test/resources/scripts/test.js
@@ -0,0 +1,6 @@
+// access_script_vars {
+// The most basic of k6 scripts.
+export default function(){
+ console.log(`k6 tests ${__ENV.MY_SCRIPT_VAR}`)
+}
+// }
diff --git a/modules/kafka/build.gradle b/modules/kafka/build.gradle
index ccc9749db6d..aabad9553b0 100644
--- a/modules/kafka/build.gradle
+++ b/modules/kafka/build.gradle
@@ -3,7 +3,7 @@ description = "Testcontainers :: Kafka"
dependencies {
api project(':testcontainers')
- testImplementation 'org.apache.kafka:kafka-clients:3.3.1'
- testImplementation 'org.assertj:assertj-core:3.23.1'
+ testImplementation 'org.apache.kafka:kafka-clients:4.3.1'
testImplementation 'com.google.guava:guava:23.0'
+ testImplementation 'org.awaitility:awaitility:4.3.0'
}
diff --git a/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java b/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java
index 02342351b83..7eb836ade18 100644
--- a/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java
+++ b/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java
@@ -1,13 +1,36 @@
package org.testcontainers.containers;
import com.github.dockerjava.api.command.InspectContainerResponse;
-import lombok.SneakyThrows;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.images.builder.Transferable;
+import org.testcontainers.utility.ComparableVersion;
import org.testcontainers.utility.DockerImageName;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
/**
- * This container wraps Confluent Kafka and Zookeeper (optionally)
+ * Testcontainers implementation for Apache Kafka.
+ * Zookeeper can be optionally configured.
+ *
+ * Supported image: {@code confluentinc/cp-kafka}
+ *
+ * Exposed ports:
+ *
+ * Kafka: 9093
+ * Zookeeper: 2181
+ *
*
+ * @deprecated use {@link org.testcontainers.kafka.ConfluentKafkaContainer} or
+ * {@link org.testcontainers.kafka.KafkaContainer} instead
*/
+@Deprecated
public class KafkaContainer extends GenericContainer {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("confluentinc/cp-kafka");
@@ -20,10 +43,21 @@ public class KafkaContainer extends GenericContainer {
private static final String DEFAULT_INTERNAL_TOPIC_RF = "1";
+ private static final String STARTER_SCRIPT = "/tmp/testcontainers_start.sh";
+
+ // https://docs.confluent.io/platform/7.0.0/release-notes/index.html#ak-raft-kraft
+ private static final String MIN_KRAFT_TAG = "7.0.0";
+
+ public static final String DEFAULT_CLUSTER_ID = "4L6g3nShT-eMCtK--X86sw";
+
protected String externalZookeeperConnect = null;
+ private boolean kraftEnabled = false;
+
+ private static final String PROTOCOL_PREFIX = "TC";
+
/**
- * @deprecated use {@link KafkaContainer(DockerImageName)} instead
+ * @deprecated use {@link #KafkaContainer(DockerImageName)} instead
*/
@Deprecated
public KafkaContainer() {
@@ -31,7 +65,7 @@ public KafkaContainer() {
}
/**
- * @deprecated use {@link KafkaContainer(DockerImageName)} instead
+ * @deprecated use {@link #KafkaContainer(DockerImageName)} instead
*/
@Deprecated
public KafkaContainer(String confluentPlatformVersion) {
@@ -41,31 +75,64 @@ public KafkaContainer(String confluentPlatformVersion) {
public KafkaContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ }
- withExposedPorts(KAFKA_PORT);
-
- // Use two listeners with different names, it will force Kafka to communicate with itself via internal
- // listener when KAFKA_INTER_BROKER_LISTENER_NAME is set, otherwise Kafka will try to use the advertised listener
- withEnv("KAFKA_LISTENERS", "PLAINTEXT://0.0.0.0:" + KAFKA_PORT + ",BROKER://0.0.0.0:9092");
- withEnv("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "BROKER:PLAINTEXT,PLAINTEXT:PLAINTEXT");
- withEnv("KAFKA_INTER_BROKER_LISTENER_NAME", "BROKER");
+ @Override
+ KafkaContainerDef createContainerDef() {
+ return new KafkaContainerDef();
+ }
- withEnv("KAFKA_BROKER_ID", "1");
- withEnv("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF);
- withEnv("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", DEFAULT_INTERNAL_TOPIC_RF);
- withEnv("KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF);
- withEnv("KAFKA_TRANSACTION_STATE_LOG_MIN_ISR", DEFAULT_INTERNAL_TOPIC_RF);
- withEnv("KAFKA_LOG_FLUSH_INTERVAL_MESSAGES", Long.MAX_VALUE + "");
- withEnv("KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS", "0");
+ @Override
+ KafkaContainerDef getContainerDef() {
+ return (KafkaContainerDef) super.getContainerDef();
}
public KafkaContainer withEmbeddedZookeeper() {
- externalZookeeperConnect = null;
+ if (this.kraftEnabled) {
+ throw new IllegalStateException("Cannot configure Zookeeper when using Kraft mode");
+ }
+ this.externalZookeeperConnect = null;
return self();
}
public KafkaContainer withExternalZookeeper(String connectString) {
- externalZookeeperConnect = connectString;
+ if (this.kraftEnabled) {
+ throw new IllegalStateException("Cannot configure Zookeeper when using Kraft mode");
+ }
+ this.externalZookeeperConnect = connectString;
+ return self();
+ }
+
+ public KafkaContainer withKraft() {
+ if (this.externalZookeeperConnect != null) {
+ throw new IllegalStateException("Cannot configure Kraft mode when Zookeeper configured");
+ }
+ verifyMinKraftVersion();
+ this.kraftEnabled = true;
+ return self();
+ }
+
+ private void verifyMinKraftVersion() {
+ String actualVersion = DockerImageName.parse(getDockerImageName()).getVersionPart();
+ if (new ComparableVersion(actualVersion).isLessThan(MIN_KRAFT_TAG)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Provided Confluent Platform's version %s is not supported in Kraft mode (must be %s or above)",
+ actualVersion,
+ MIN_KRAFT_TAG
+ )
+ );
+ }
+ }
+
+ private boolean isLessThanCP740() {
+ String actualVersion = DockerImageName.parse(getDockerImageName()).getVersionPart();
+ return new ComparableVersion(actualVersion).isLessThan("7.4.0");
+ }
+
+ public KafkaContainer withClusterId(String clusterId) {
+ Objects.requireNonNull(clusterId, "clusterId cannot be null");
+ getContainerDef().withClusterId(clusterId);
return self();
}
@@ -75,52 +142,219 @@ public String getBootstrapServers() {
@Override
protected void configure() {
- withEnv(
- "KAFKA_ADVERTISED_LISTENERS",
- String.format("BROKER://%s:9092", getNetwork() != null ? getNetworkAliases().get(0) : "localhost")
- );
+ getContainerDef().resolveListeners();
- String command = "#!/bin/bash\n";
- if (externalZookeeperConnect != null) {
- withEnv("KAFKA_ZOOKEEPER_CONNECT", externalZookeeperConnect);
+ if (this.kraftEnabled) {
+ configureKraft();
} else {
- addExposedPort(ZOOKEEPER_PORT);
- withEnv("KAFKA_ZOOKEEPER_CONNECT", "localhost:" + ZOOKEEPER_PORT);
- command += "echo 'clientPort=" + ZOOKEEPER_PORT + "' > zookeeper.properties\n";
- command += "echo 'dataDir=/var/lib/zookeeper/data' >> zookeeper.properties\n";
- command += "echo 'dataLogDir=/var/lib/zookeeper/log' >> zookeeper.properties\n";
- command += "zookeeper-server-start zookeeper.properties &\n";
+ configureZookeeper();
}
+ }
- // Optimization: skip the checks
- command += "echo '' > /etc/confluent/docker/ensure \n";
- // Run the original command
- command += "/etc/confluent/docker/run \n";
- withCommand("sh", "-c", command);
+ protected void configureKraft() {
+ getContainerDef().withRaft();
+ }
+
+ protected void configureZookeeper() {
+ if (this.externalZookeeperConnect == null) {
+ getContainerDef().withEmbeddedZookeeper();
+ } else {
+ getContainerDef().withZookeeper(this.externalZookeeperConnect);
+ }
}
@Override
- @SneakyThrows
- protected void containerIsStarted(InspectContainerResponse containerInfo) {
- String brokerAdvertisedListener = brokerAdvertisedListener(containerInfo);
- ExecResult result = execInContainer(
- "kafka-configs",
- "--alter",
- "--bootstrap-server",
- brokerAdvertisedListener,
- "--entity-type",
- "brokers",
- "--entity-name",
- getEnvMap().get("KAFKA_BROKER_ID"),
- "--add-config",
- "advertised.listeners=[" + String.join(",", getBootstrapServers(), brokerAdvertisedListener) + "]"
- );
- if (result.getExitCode() != 0) {
- throw new IllegalStateException(result.toString());
+ protected void containerIsStarting(InspectContainerResponse containerInfo) {
+ super.containerIsStarting(containerInfo);
+
+ List advertisedListeners = new ArrayList<>();
+ advertisedListeners.add(getBootstrapServers());
+ advertisedListeners.add(brokerAdvertisedListener(containerInfo));
+
+ List> listenersToTransform = new ArrayList<>(getContainerDef().listeners);
+ for (int i = 0; i < listenersToTransform.size(); i++) {
+ Supplier listenerSupplier = listenersToTransform.get(i);
+ String protocol = String.format("%s-%d", PROTOCOL_PREFIX, i);
+ String listener = listenerSupplier.get();
+ String listenerProtocol = String.format("%s://%s", protocol, listener);
+ advertisedListeners.add(listenerProtocol);
+ }
+
+ String kafkaAdvertisedListeners = String.join(",", advertisedListeners);
+
+ String command = "#!/bin/bash\n";
+ // exporting KAFKA_ADVERTISED_LISTENERS with the container hostname
+ command += String.format("export KAFKA_ADVERTISED_LISTENERS=%s\n", kafkaAdvertisedListeners);
+
+ if (!this.kraftEnabled || isLessThanCP740()) {
+ // Optimization: skip the checks
+ command += "echo '' > /etc/confluent/docker/ensure \n";
}
+
+ if (this.kraftEnabled) {
+ command += commandKraft();
+ } else if (this.externalZookeeperConnect == null) {
+ command += commandZookeeper();
+ }
+
+ // Run the original command
+ command += "/etc/confluent/docker/run \n";
+ copyFileToContainer(Transferable.of(command, 0777), STARTER_SCRIPT);
+ }
+
+ protected String commandKraft() {
+ String command = "sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure\n";
+ command +=
+ "echo 'kafka-storage format --ignore-formatted -t \"" +
+ getContainerDef().getEnvVars().get("CLUSTER_ID") +
+ "\" -c /etc/kafka/kafka.properties' >> /etc/confluent/docker/configure\n";
+ return command;
+ }
+
+ protected String commandZookeeper() {
+ String command = "echo 'clientPort=" + ZOOKEEPER_PORT + "' > /tmp/zookeeper.properties\n";
+ command += "echo 'dataDir=/var/lib/zookeeper/data' >> /tmp/zookeeper.properties\n";
+ command += "echo 'dataLogDir=/var/lib/zookeeper/log' >> /tmp/zookeeper.properties\n";
+ command += "zookeeper-server-start /tmp/zookeeper.properties &\n";
+ return command;
+ }
+
+ /**
+ * Add a {@link Supplier} that will provide a listener with format {@code host:port}.
+ * Host will be added as a network alias.
+ *
+ * The listener will be added to the list of default listeners.
+ *
+ * Default listeners:
+ *
+ * 0.0.0.0:9092
+ * 0.0.0.0:9093
+ *
+ *
+ * Default advertised listeners:
+ *
+ * {@code container.getHost():container.getMappedPort(9093)}
+ * {@code container.getConfig().getHostName():9092}
+ *
+ * @param listenerSupplier a supplier that will provide a listener
+ * @return this {@link KafkaContainer} instance
+ */
+ public KafkaContainer withListener(Supplier listenerSupplier) {
+ getContainerDef().withListener(listenerSupplier);
+ return this;
}
protected String brokerAdvertisedListener(InspectContainerResponse containerInfo) {
return String.format("BROKER://%s:%s", containerInfo.getConfig().getHostName(), "9092");
}
+
+ private static class KafkaContainerDef extends ContainerDef {
+
+ private final Set> listeners = new HashSet<>();
+
+ private String clusterId = DEFAULT_CLUSTER_ID;
+
+ KafkaContainerDef() {
+ // Use two listeners with different names, it will force Kafka to communicate with itself via internal
+ // listener when KAFKA_INTER_BROKER_LISTENER_NAME is set, otherwise Kafka will try to use the advertised listener
+ addEnvVar("KAFKA_LISTENERS", "PLAINTEXT://0.0.0.0:" + KAFKA_PORT + ",BROKER://0.0.0.0:9092");
+ addEnvVar("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "BROKER:PLAINTEXT,PLAINTEXT:PLAINTEXT");
+ addEnvVar("KAFKA_INTER_BROKER_LISTENER_NAME", "BROKER");
+
+ addEnvVar("KAFKA_BROKER_ID", "1");
+ addEnvVar("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF);
+ addEnvVar("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", DEFAULT_INTERNAL_TOPIC_RF);
+ addEnvVar("KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF);
+ addEnvVar("KAFKA_TRANSACTION_STATE_LOG_MIN_ISR", DEFAULT_INTERNAL_TOPIC_RF);
+ addEnvVar("KAFKA_LOG_FLUSH_INTERVAL_MESSAGES", Long.MAX_VALUE + "");
+ addEnvVar("KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS", "0");
+
+ addExposedTcpPort(KAFKA_PORT);
+
+ setEntrypoint("sh");
+ setCommand("-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT);
+
+ setWaitStrategy(Wait.forLogMessage(".*\\[KafkaServer id=\\d+\\] started.*", 1));
+ }
+
+ private void resolveListeners() {
+ Set