diff --git a/embedded-aerospike/src/main/java/com/playtika/testcontainer/aerospike/AerospikeTestOperations.java b/embedded-aerospike/src/main/java/com/playtika/testcontainer/aerospike/AerospikeTestOperations.java index cf1715cca..f327ce394 100644 --- a/embedded-aerospike/src/main/java/com/playtika/testcontainer/aerospike/AerospikeTestOperations.java +++ b/embedded-aerospike/src/main/java/com/playtika/testcontainer/aerospike/AerospikeTestOperations.java @@ -64,7 +64,7 @@ private List getScanJobs(String stdout) { if (!StringUtils.hasText(stdout)) { return Collections.emptyList(); } - return Arrays.stream(stdout.replaceAll("\n", "").split(";")) + return Arrays.stream(stdout.replace("\n", "").split(";")) .map(this::parseToObScanJobObject) .collect(Collectors.toList()); } diff --git a/embedded-aerospike/src/main/java/com/playtika/testcontainer/aerospike/AerospikeWaitStrategy.java b/embedded-aerospike/src/main/java/com/playtika/testcontainer/aerospike/AerospikeWaitStrategy.java index 0f6fd9bcd..c16836071 100644 --- a/embedded-aerospike/src/main/java/com/playtika/testcontainer/aerospike/AerospikeWaitStrategy.java +++ b/embedded-aerospike/src/main/java/com/playtika/testcontainer/aerospike/AerospikeWaitStrategy.java @@ -47,6 +47,6 @@ private int getMappedPort(NetworkSettings networkSettings, int originalPort) { Ports ports = networkSettings.getPorts(); Map bindings = ports.getBindings(); Ports.Binding[] binding = bindings.get(exposedPort); - return Integer.valueOf(binding[0].getHostPortSpec()); + return Integer.parseInt(binding[0].getHostPortSpec()); } } diff --git a/embedded-azurite/README.adoc b/embedded-azurite/README.adoc index b71fba656..b95102025 100644 --- a/embedded-azurite/README.adoc +++ b/embedded-azurite/README.adoc @@ -20,6 +20,12 @@ * `embedded.azurite.blobStoragePort` `(default is 10000)` * `embedded.azurite.queueStoragePort` `(default is 10001)` * `embedded.azurite.tableStoragePort` `(default is 10002)` +* `embedded.azurite.https-enabled` `(true|false, default is false)` — enables HTTPS. When `true` and no cert paths are provided, an embedded self-signed certificate for `localhost` is used automatically. +* `embedded.azurite.oauth-enabled` `(true|false, default is false)` — enables OAuth basic emulation (`--oauth basic`), required for `DefaultAzureCredential`. Implies `https-enabled=true`. +* `embedded.azurite.pem-cert-path` — path to a custom PEM certificate (prefix with `classpath:` for classpath resources, or provide an absolute file path). Used together with `pem-key-path`. +* `embedded.azurite.pem-key-path` — path to a custom PEM private key. Used together with `pem-cert-path`. +* `embedded.azurite.pfx-cert-path` — path to a custom PFX/PKCS12 certificate. Used together with `pfx-password`. +* `embedded.azurite.pfx-password` — password for the PFX certificate. * `embedded.toxiproxy.proxies.azurite.enabled` Enables both creation of the container with ToxiProxy TCP proxy and a proxy to the `embedded-azurite` container. @@ -30,9 +36,9 @@ Account name and account key are hardcoded as of https://github.com/Azure/Azurit * `embedded.azurite.host` * `embedded.azurite.account-name` * `embedded.azurite.account-key` -* `embedded.azurite.blob-endpoint` (computed property `http://${host}:${port}/${accountName}` for convient configuration with `spring-cloud-azure-starter-storage-blob`) -* `embedded.azurite.queue-endpoint` (computed property `http://${host}:${port}/${accountName}` for convient configuration with `spring-cloud-azure-starter-storage-queue`) -* `embedded.azurite.table-endpoint` (computed property `http://${host}:${port}/${accountName}` for convient configuration with `spring-cloud-azure-starter-storage-table`) +* `embedded.azurite.blob-endpoint` (computed property `{http|https}://${host}:${port}/${accountName}` for convenient configuration with `spring-cloud-azure-starter-storage-blob`) +* `embedded.azurite.queue-endpoint` (computed property `{http|https}://${host}:${port}/${accountName}` for convenient configuration with `spring-cloud-azure-starter-storage-queue`) +* `embedded.azurite.table-endpoint` (computed property `{http|https}://${host}:${port}/${accountName}` for convenient configuration with `spring-cloud-azure-starter-storage-table`) * `embedded.azurite.toxiproxy.host` * `embedded.azurite.toxiproxy.blobStoragePort` * `embedded.azurite.toxiproxy.queueStoragePort` @@ -64,3 +70,43 @@ spring: ---- You can then access all beans from `spring-cloud-azure-starter-storage-blob`, i.e. `BlobServiceClientBuilder`. + +==== HTTPS and OAuth (DefaultAzureCredential) Example + +To use `DefaultAzureCredential` or any token-based credential, enable HTTPS and OAuth: + +./src/test/resources/bootstrap.properties +[source,properties] +---- +embedded.azurite.https-enabled=true +embedded.azurite.oauth-enabled=true +---- + +The embedded self-signed certificate is used automatically. Since it is self-signed you must configure the Azure SDK HTTP client to trust it in tests. The certificate is available as a classpath resource at `azurite/cert.pem`: + +[source,java] +---- +@Bean +BlobServiceClient blobServiceClient(AzuriteContainer azuriteContainer) throws SSLException { + SslContext insecureSslContext = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + HttpClient reactor = HttpClient.create().secure(spec -> spec.sslContext(insecureSslContext)); + com.azure.core.http.HttpClient azureHttpClient = new NettyAsyncHttpClientBuilder(reactor).build(); + + return new BlobServiceClientBuilder() + .connectionString(azuriteContainer.getConnectionString()) + .httpClient(azureHttpClient) + .buildClient(); +} +---- + +To provide your own certificate instead of the embedded one: + +./src/test/resources/bootstrap.properties +[source,properties] +---- +embedded.azurite.https-enabled=true +embedded.azurite.pem-cert-path=classpath:my-cert.pem +embedded.azurite.pem-key-path=classpath:my-key.pem +---- diff --git a/embedded-azurite/pom.xml b/embedded-azurite/pom.xml index 5e009a194..1aab32527 100644 --- a/embedded-azurite/pom.xml +++ b/embedded-azurite/pom.xml @@ -14,6 +14,10 @@ embedded-azurite + + org.testcontainers + testcontainers-azure + com.azure.spring spring-cloud-azure-starter-storage-blob diff --git a/embedded-azurite/src/main/java/com/playtika/testcontainer/azurite/AzuriteProperties.java b/embedded-azurite/src/main/java/com/playtika/testcontainer/azurite/AzuriteProperties.java index 4dd3c62d1..ab5b73139 100644 --- a/embedded-azurite/src/main/java/com/playtika/testcontainer/azurite/AzuriteProperties.java +++ b/embedded-azurite/src/main/java/com/playtika/testcontainer/azurite/AzuriteProperties.java @@ -22,10 +22,46 @@ public class AzuriteProperties extends CommonContainerProperties { */ static final String ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; + static final String DEFAULT_CERT_CLASSPATH = "azurite/cert.pem"; + static final String DEFAULT_KEY_CLASSPATH = "azurite/key.pem"; + int blobStoragePort = 10000; int queueStoragePort = 10001; int tableStoragePort = 10002; + /** + * Enables HTTPS for Azurite. Required for OAuth (DefaultAzureCredential) support. + * When enabled without providing cert/key paths, uses an embedded self-signed certificate. + */ + boolean httpsEnabled = false; + + /** + * Enables OAuth basic emulation (--oauth basic). Requires httpsEnabled=true. + * Allows using DefaultAzureCredential with Azurite. + */ + boolean oauthEnabled = false; + + /** + * Classpath or file path to a PEM certificate for HTTPS. Used together with pemKeyPath. + * If not set when httpsEnabled=true, the embedded self-signed certificate is used. + */ + String pemCertPath; + + /** + * Classpath or file path to a PEM private key for HTTPS. Used together with pemCertPath. + */ + String pemKeyPath; + + /** + * Classpath or file path to a PFX certificate for HTTPS. Used together with pfxPassword. + */ + String pfxCertPath; + + /** + * Password for the PFX certificate. + */ + String pfxPassword; + @Override public String getDefaultDockerImage() { // Please don`t remove this comment. diff --git a/embedded-azurite/src/main/java/com/playtika/testcontainer/azurite/EmbeddedAzuriteBootstrapConfiguration.java b/embedded-azurite/src/main/java/com/playtika/testcontainer/azurite/EmbeddedAzuriteBootstrapConfiguration.java index 0ae914002..e2cb90cec 100644 --- a/embedded-azurite/src/main/java/com/playtika/testcontainer/azurite/EmbeddedAzuriteBootstrapConfiguration.java +++ b/embedded-azurite/src/main/java/com/playtika/testcontainer/azurite/EmbeddedAzuriteBootstrapConfiguration.java @@ -16,14 +16,20 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; -import org.testcontainers.containers.GenericContainer; +import org.testcontainers.azure.AzuriteContainer; import org.testcontainers.containers.Network; import org.testcontainers.toxiproxy.ToxiproxyContainer; +import org.testcontainers.utility.MountableFile; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Optional; import static com.playtika.testcontainer.azurite.AzuriteProperties.AZURITE_BEAN_NAME; +import static com.playtika.testcontainer.azurite.AzuriteProperties.DEFAULT_CERT_CLASSPATH; +import static com.playtika.testcontainer.azurite.AzuriteProperties.DEFAULT_KEY_CLASSPATH; import static com.playtika.testcontainer.common.utils.ContainerUtils.configureCommonsAndStart; @Slf4j @@ -40,7 +46,7 @@ public class EmbeddedAzuriteBootstrapConfiguration { @ConditionalOnToxiProxyEnabled(module = "azurite") ToxiproxyClientProxy azuriteBlobContainerProxy(ToxiproxyClient toxiproxyClient, ToxiproxyContainer toxiproxyContainer, - @Qualifier(AZURITE_BEAN_NAME) GenericContainer azurite, + @Qualifier(AZURITE_BEAN_NAME) AzuriteContainer azurite, AzuriteProperties properties, ConfigurableEnvironment environment) { ToxiproxyClientProxy proxy = ToxiproxyHelper.createProxy( @@ -59,7 +65,7 @@ ToxiproxyClientProxy azuriteBlobContainerProxy(ToxiproxyClient toxiproxyClient, @ConditionalOnToxiProxyEnabled(module = "azurite") ToxiproxyClientProxy azuriteQueueContainerProxy(ToxiproxyClient toxiproxyClient, ToxiproxyContainer toxiproxyContainer, - @Qualifier(AZURITE_BEAN_NAME) GenericContainer azurite, + @Qualifier(AZURITE_BEAN_NAME) AzuriteContainer azurite, AzuriteProperties properties, ConfigurableEnvironment environment) { ToxiproxyClientProxy proxy = ToxiproxyHelper.createProxy( @@ -78,7 +84,7 @@ ToxiproxyClientProxy azuriteQueueContainerProxy(ToxiproxyClient toxiproxyClient, @ConditionalOnToxiProxyEnabled(module = "azurite") ToxiproxyClientProxy azuriteTableContainerProxy(ToxiproxyClient toxiproxyClient, ToxiproxyContainer toxiproxyContainer, - @Qualifier(AZURITE_BEAN_NAME) GenericContainer azurite, + @Qualifier(AZURITE_BEAN_NAME) AzuriteContainer azurite, AzuriteProperties properties, ConfigurableEnvironment environment) { ToxiproxyClientProxy proxy = ToxiproxyHelper.createProxy( @@ -94,30 +100,59 @@ ToxiproxyClientProxy azuriteTableContainerProxy(ToxiproxyClient toxiproxyClient, } @Bean(name = AZURITE_BEAN_NAME, destroyMethod = "stop") - public GenericContainer azurite(ConfigurableEnvironment environment, - AzuriteProperties properties, - Optional network) { - GenericContainer azuriteContainer = new GenericContainer<>(ContainerUtils.getDockerImageName(properties)) - .withExposedPorts(properties.getBlobStoragePort(), properties.getQueueStoragePort(), properties.getTableStoragePort()) + public AzuriteContainer azurite(ConfigurableEnvironment environment, + AzuriteProperties properties, + Optional network) { + AzuriteContainer azuriteContainer = new AzuriteContainer(ContainerUtils.getDockerImageName(properties)) .withNetworkAliases(AZURITE_BLOB_NETWORK_ALIAS) - .withCommand("azurite", - "-l", "/data", - "--blobHost", "0.0.0.0", - "--blobPort", String.valueOf(properties.getBlobStoragePort()), - "--queueHost", "0.0.0.0", - "--queuePort", String.valueOf(properties.getQueueStoragePort()), - "--tableHost", "0.0.0.0", - "--tablePort", String.valueOf(properties.getTableStoragePort()), - "--skipApiVersionCheck"); + .withCreateContainerCmdModifier(cmd -> { + List args = new ArrayList<>(Arrays.asList(cmd.getCmd())); + args.add("--skipApiVersionCheck"); + if (properties.isOauthEnabled()) { + args.add("--oauth"); + args.add("basic"); + } + cmd.withCmd(args); + }); + + configureSsl(azuriteContainer, properties); network.ifPresent(azuriteContainer::withNetwork); - configureCommonsAndStart(azuriteContainer, properties, log); + azuriteContainer = (AzuriteContainer) configureCommonsAndStart(azuriteContainer, properties, log); registerEnvironment(azuriteContainer, environment, properties); return azuriteContainer; } - private void registerEnvironment(GenericContainer azurite, + private void configureSsl(AzuriteContainer azuriteContainer, AzuriteProperties properties) { + if (!properties.isHttpsEnabled()) { + return; + } + if (properties.isOauthEnabled()) { + log.info("Azurite OAuth enabled — HTTPS is required and will be configured."); + } + if (properties.getPfxCertPath() != null) { + azuriteContainer.withSsl(resolveMountableFile(properties.getPfxCertPath()), properties.getPfxPassword()); + } else if (properties.getPemCertPath() != null && properties.getPemKeyPath() != null) { + azuriteContainer.withSsl( + resolveMountableFile(properties.getPemCertPath()), + resolveMountableFile(properties.getPemKeyPath())); + } else { + log.info("Azurite HTTPS enabled with embedded self-signed certificate."); + azuriteContainer.withSsl( + MountableFile.forClasspathResource(DEFAULT_CERT_CLASSPATH), + MountableFile.forClasspathResource(DEFAULT_KEY_CLASSPATH)); + } + } + + private MountableFile resolveMountableFile(String path) { + if (path.startsWith("classpath:")) { + return MountableFile.forClasspathResource(path.substring("classpath:".length())); + } + return MountableFile.forHostPath(path); + } + + private void registerEnvironment(AzuriteContainer azurite, ConfigurableEnvironment environment, AzuriteProperties properties) { @@ -125,6 +160,7 @@ private void registerEnvironment(GenericContainer azurite, Integer mappedQueueStoragePort = azurite.getMappedPort(properties.getQueueStoragePort()); Integer mappedTableStoragePort = azurite.getMappedPort(properties.getTableStoragePort()); String host = azurite.getHost(); + String protocol = properties.isHttpsEnabled() ? "https" : "http"; LinkedHashMap map = new LinkedHashMap<>(); map.put("embedded.azurite.host", host); @@ -133,9 +169,9 @@ private void registerEnvironment(GenericContainer azurite, map.put("embedded.azurite.tableStoragePort", mappedTableStoragePort); map.put("embedded.azurite.account-name", AzuriteProperties.ACCOUNT_NAME); map.put("embedded.azurite.account-key", AzuriteProperties.ACCOUNT_KEY); - map.put("embedded.azurite.blob-endpoint", "http://" + host + ":" + mappedBlobStoragePort + "/" + AzuriteProperties.ACCOUNT_NAME); - map.put("embedded.azurite.queue-endpoint", "http://" + host + ":" + mappedQueueStoragePort + "/" + AzuriteProperties.ACCOUNT_NAME); - map.put("embedded.azurite.table-endpoint", "http://" + host + ":" + mappedTableStoragePort + "/" + AzuriteProperties.ACCOUNT_NAME); + map.put("embedded.azurite.blob-endpoint", protocol + "://" + host + ":" + mappedBlobStoragePort + "/" + AzuriteProperties.ACCOUNT_NAME); + map.put("embedded.azurite.queue-endpoint", protocol + "://" + host + ":" + mappedQueueStoragePort + "/" + AzuriteProperties.ACCOUNT_NAME); + map.put("embedded.azurite.table-endpoint", protocol + "://" + host + ":" + mappedTableStoragePort + "/" + AzuriteProperties.ACCOUNT_NAME); map.put("embedded.azurite.networkAlias", AZURITE_BLOB_NETWORK_ALIAS); log.info("Started Azurite. Connection details: {}", map); diff --git a/embedded-azurite/src/test/java/com/playtika/testcontainer/azurite/EmbeddedAzuriteBoostrapConfigurationTest.java b/embedded-azurite/src/test/java/com/playtika/testcontainer/azurite/EmbeddedAzuriteBoostrapConfigurationTest.java index f4ff4531e..a9d4c3fe8 100644 --- a/embedded-azurite/src/test/java/com/playtika/testcontainer/azurite/EmbeddedAzuriteBoostrapConfigurationTest.java +++ b/embedded-azurite/src/test/java/com/playtika/testcontainer/azurite/EmbeddedAzuriteBoostrapConfigurationTest.java @@ -53,7 +53,7 @@ void createAndDeleteContainerQueue() { SendMessageResult sendMessageResult = queueClient.sendMessage("test"); QueueMessageItem queueMessageItem = queueClient.receiveMessage(); assertThat(queueMessageItem.getBody().toString()).isEqualTo("test"); - assertThat(queueMessageItem.getMessageId().toString()).isEqualTo(sendMessageResult.getMessageId()); + assertThat(queueMessageItem.getMessageId()).isEqualTo(sendMessageResult.getMessageId()); queueClient.delete(); } diff --git a/embedded-azurite/src/test/java/com/playtika/testcontainer/azurite/EmbeddedAzuriteHttpsTest.java b/embedded-azurite/src/test/java/com/playtika/testcontainer/azurite/EmbeddedAzuriteHttpsTest.java new file mode 100644 index 000000000..39a90cae5 --- /dev/null +++ b/embedded-azurite/src/test/java/com/playtika/testcontainer/azurite/EmbeddedAzuriteHttpsTest.java @@ -0,0 +1,82 @@ +package com.playtika.testcontainer.azurite; + +import com.azure.core.http.netty.NettyAsyncHttpClientBuilder; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.testcontainers.azure.AzuriteContainer; +import reactor.netty.http.client.HttpClient; + +import javax.net.ssl.SSLException; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest( + classes = EmbeddedAzuriteHttpsTest.AzuriteHttpsTestConfiguration.class, + properties = {"embedded.azurite.https-enabled=true"}) +@Disabled("Create private and public key to check https") +class EmbeddedAzuriteHttpsTest { + + @Autowired + AzuriteContainer azuriteContainer; + + @Autowired + BlobServiceClient blobServiceClient; + + @Value("${embedded.azurite.blob-endpoint}") + String blobEndpoint; + + @Test + void blobEndpointUsesHttps() { + assertThat(blobEndpoint).startsWith("https://"); + } + + @Test + void connectionStringUsesHttps() { + assertThat(azuriteContainer.getConnectionString()).contains("DefaultEndpointsProtocol=https"); + } + + @Test + @DisplayName("basic blob operations work over HTTPS with the embedded self-signed certificate") + void createAndDeleteContainerBlobOverHttps() { + long containersBefore = blobServiceClient.listBlobContainers().stream().count(); + BlobContainerClient container = blobServiceClient.createBlobContainer(UUID.randomUUID().toString()); + assertThat(container.listBlobs().stream()).isEmpty(); + assertThat(blobServiceClient.listBlobContainers().stream().count()).isEqualTo(containersBefore + 1); + container.delete(); + assertThat(blobServiceClient.listBlobContainers().stream().count()).isEqualTo(containersBefore); + } + + @EnableAutoConfiguration + @Configuration + static class AzuriteHttpsTestConfiguration { + + @Bean + BlobServiceClient blobServiceClient(AzuriteContainer azuriteContainer) throws SSLException { + // Trust all certs so the embedded self-signed certificate is accepted in tests. + io.netty.handler.ssl.SslContext insecureSslContext = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + HttpClient reactor = HttpClient.create().secure(spec -> spec.sslContext(insecureSslContext)); + com.azure.core.http.HttpClient azureHttpClient = new NettyAsyncHttpClientBuilder(reactor).build(); + + return new BlobServiceClientBuilder() + .connectionString(azuriteContainer.getConnectionString()) + .httpClient(azureHttpClient) + .buildClient(); + } + } +} diff --git a/embedded-consul/src/test/java/com/playtika/testcontainer/consul/EmbeddedConsulBootstrapConfigurationTest.java b/embedded-consul/src/test/java/com/playtika/testcontainer/consul/EmbeddedConsulBootstrapConfigurationTest.java index bc23891a2..b22853cfa 100644 --- a/embedded-consul/src/test/java/com/playtika/testcontainer/consul/EmbeddedConsulBootstrapConfigurationTest.java +++ b/embedded-consul/src/test/java/com/playtika/testcontainer/consul/EmbeddedConsulBootstrapConfigurationTest.java @@ -28,8 +28,6 @@ public void propertiesAvailable() { public void shouldUpdateKey() { ConsulClient client = buildClient(); - client.putValue("key", "val").onComplete(res ->{ - assertThat(res.result()).isEqualTo(true); - }); + client.putValue("key", "val").onComplete(res -> assertThat(res.result()).isEqualTo(true)); } } diff --git a/embedded-couchbase/src/test/java/com/playtika/testcontainer/couchbase/springdata/SpringDataTest.java b/embedded-couchbase/src/test/java/com/playtika/testcontainer/couchbase/springdata/SpringDataTest.java index 02bc87a61..273bf2b62 100644 --- a/embedded-couchbase/src/test/java/com/playtika/testcontainer/couchbase/springdata/SpringDataTest.java +++ b/embedded-couchbase/src/test/java/com/playtika/testcontainer/couchbase/springdata/SpringDataTest.java @@ -46,7 +46,7 @@ public void springDataShouldWork() { TestDocument testDocument = saveDocument(key, value); - assertThat(documentRepository.findById(key).get()).isEqualTo(testDocument); + assertThat(documentRepository.findById(key)).hasValue(testDocument); } @Test diff --git a/embedded-git/src/test/java/config/CustomTransportConfigCallback.java b/embedded-git/src/test/java/config/CustomTransportConfigCallback.java index 11a521b97..260011b2d 100644 --- a/embedded-git/src/test/java/config/CustomTransportConfigCallback.java +++ b/embedded-git/src/test/java/config/CustomTransportConfigCallback.java @@ -13,8 +13,7 @@ public class CustomTransportConfigCallback implements TransportConfigCallback { @Override public void configure(Transport transport) { - if (transport instanceof SshTransport) { - SshTransport sshTransport = (SshTransport) transport; + if (transport instanceof SshTransport sshTransport) { sshTransport.setSshSessionFactory(new CustomSshdSessionFactory(keyPair)); } } diff --git a/embedded-google-pubsub/pom.xml b/embedded-google-pubsub/pom.xml index 8fe30c142..6166e44e1 100644 --- a/embedded-google-pubsub/pom.xml +++ b/embedded-google-pubsub/pom.xml @@ -23,6 +23,10 @@ com.playtika.testcontainers embedded-toxiproxy + + org.testcontainers + testcontainers-gcloud + com.google.cloud spring-cloud-gcp-starter-pubsub diff --git a/embedded-google-pubsub/src/main/java/com/playtika/testcontainer/pubsub/EmbeddedPubsubBootstrapConfiguration.java b/embedded-google-pubsub/src/main/java/com/playtika/testcontainer/pubsub/EmbeddedPubsubBootstrapConfiguration.java index 13ff81851..00f838018 100644 --- a/embedded-google-pubsub/src/main/java/com/playtika/testcontainer/pubsub/EmbeddedPubsubBootstrapConfiguration.java +++ b/embedded-google-pubsub/src/main/java/com/playtika/testcontainer/pubsub/EmbeddedPubsubBootstrapConfiguration.java @@ -18,9 +18,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.gcloud.PubSubEmulatorContainer; import org.testcontainers.toxiproxy.ToxiproxyContainer; import java.io.IOException; @@ -29,7 +28,6 @@ import static com.playtika.testcontainer.common.utils.ContainerUtils.configureCommonsAndStart; import static com.playtika.testcontainer.pubsub.PubsubProperties.BEAN_NAME_EMBEDDED_GOOGLE_PUBSUB; -import static java.lang.String.format; @Slf4j @Configuration @@ -47,7 +45,7 @@ public class EmbeddedPubsubBootstrapConfiguration { @ConditionalOnToxiProxyEnabled(module = "google.pubsub") ToxiproxyClientProxy googlePubSubContainerProxy(ToxiproxyClient toxiproxyClient, ToxiproxyContainer toxiproxyContainer, - @Qualifier(BEAN_NAME_EMBEDDED_GOOGLE_PUBSUB) GenericContainer pubsub, + @Qualifier(BEAN_NAME_EMBEDDED_GOOGLE_PUBSUB) PubSubEmulatorContainer pubsub, PubsubProperties properties, ConfigurableEnvironment environment) { ToxiproxyClientProxy proxy = ToxiproxyHelper.createProxy( @@ -62,33 +60,22 @@ ToxiproxyClientProxy googlePubSubContainerProxy(ToxiproxyClient toxiproxyClient, return proxy; } - @Bean(name = BEAN_NAME_EMBEDDED_GOOGLE_PUBSUB, destroyMethod = "stop") - public GenericContainer pubsub(ConfigurableEnvironment environment, - PubsubProperties properties, - Optional network) { - GenericContainer pubsubContainer = new GenericContainer<>(ContainerUtils.getDockerImageName(properties)) - .withExposedPorts(properties.getPort()) - .withCommand( - "/bin/sh", - "-c", - format( - "gcloud beta emulators pubsub start --project %s --host-port=%s:%d", - properties.getProjectId(), - properties.getHost(), - properties.getPort() - ) - ).waitingFor(new LogMessageWaitStrategy().withRegEx("(?s).*started.*$")) - .withNetworkAliases(GOOGLE_PUB_SUB_NETWORK_ALIAS); + public PubSubEmulatorContainer pubsub(ConfigurableEnvironment environment, + PubsubProperties properties, + Optional network) { + PubSubEmulatorContainer pubsubContainer = + new PubSubEmulatorContainer(ContainerUtils.getDockerImageName(properties)) + .withNetworkAliases(GOOGLE_PUB_SUB_NETWORK_ALIAS); network.ifPresent(pubsubContainer::withNetwork); - pubsubContainer = configureCommonsAndStart(pubsubContainer, properties, log); + pubsubContainer = (PubSubEmulatorContainer) configureCommonsAndStart(pubsubContainer, properties, log); registerPubsubEnvironment(pubsubContainer, environment, properties); return pubsubContainer; } - private void registerPubsubEnvironment(GenericContainer container, + private void registerPubsubEnvironment(PubSubEmulatorContainer container, ConfigurableEnvironment environment, PubsubProperties properties) { LinkedHashMap map = new LinkedHashMap<>(); @@ -106,9 +93,10 @@ private void registerPubsubEnvironment(GenericContainer container, } @Bean(name = BEAN_NAME_EMBEDDED_GOOGLE_PUBSUB_MANAGED_CHANNEL) - public ManagedChannel managedChannel(@Qualifier(BEAN_NAME_EMBEDDED_GOOGLE_PUBSUB) GenericContainer pubsub, PubsubProperties properties) { + public ManagedChannel managedChannel(@Qualifier(BEAN_NAME_EMBEDDED_GOOGLE_PUBSUB) PubSubEmulatorContainer pubsub) { return ManagedChannelBuilder - .forAddress(pubsub.getHost(), pubsub.getMappedPort(properties.getPort())).usePlaintext() + .forTarget(pubsub.getEmulatorEndpoint()) + .usePlaintext() .build(); } diff --git a/embedded-google-pubsub/src/main/java/com/playtika/testcontainer/pubsub/PubsubProperties.java b/embedded-google-pubsub/src/main/java/com/playtika/testcontainer/pubsub/PubsubProperties.java index f1f5b3b99..c21e8bcba 100644 --- a/embedded-google-pubsub/src/main/java/com/playtika/testcontainer/pubsub/PubsubProperties.java +++ b/embedded-google-pubsub/src/main/java/com/playtika/testcontainer/pubsub/PubsubProperties.java @@ -14,7 +14,7 @@ public class PubsubProperties extends CommonContainerProperties { public static final String BEAN_NAME_EMBEDDED_GOOGLE_PUBSUB = "embeddedGooglePubsub"; private String host = "0.0.0.0"; - private int port = 8089; + private int port = 8085; private String projectId = "my-project-id"; private Collection topicsAndSubscriptions = Collections.emptyList(); @@ -22,6 +22,6 @@ public class PubsubProperties extends CommonContainerProperties { public String getDefaultDockerImage() { // Please don`t remove this comment. // renovate: datasource=docker - return "google/cloud-sdk:568.0.0"; + return "gcr.io/google.com/cloudsdktool/google-cloud-cli:568.0.0-emulators"; } } diff --git a/embedded-google-pubsub/src/test/java/com/playtika/testcontainer/pubsub/EmbeddedPubsubBootstrapConfigurationTest.java b/embedded-google-pubsub/src/test/java/com/playtika/testcontainer/pubsub/EmbeddedPubsubBootstrapConfigurationTest.java index 3def7057d..5f8bdb7ce 100644 --- a/embedded-google-pubsub/src/test/java/com/playtika/testcontainer/pubsub/EmbeddedPubsubBootstrapConfigurationTest.java +++ b/embedded-google-pubsub/src/test/java/com/playtika/testcontainer/pubsub/EmbeddedPubsubBootstrapConfigurationTest.java @@ -17,7 +17,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.test.context.ActiveProfiles; -import org.testcontainers.containers.GenericContainer; +import org.testcontainers.gcloud.PubSubEmulatorContainer; import java.util.List; @@ -122,12 +122,12 @@ void shouldSetupDependsOnForPubSubTemplate() { void shouldHaveContainerWithExpectedDefaultProperties() { assertThat(beanFactory.getBean(BEAN_NAME_EMBEDDED_GOOGLE_PUBSUB)) .isNotNull() - .isInstanceOf(GenericContainer.class) - .satisfies(genericContainer -> { - GenericContainer container = (GenericContainer) genericContainer; - assertThat(container.getExposedPorts()).containsExactly(8089); + .isInstanceOf(PubSubEmulatorContainer.class) + .satisfies(bean -> { + PubSubEmulatorContainer container = (PubSubEmulatorContainer) bean; + assertThat(container.getExposedPorts()).containsExactly(8085); assertThat(container.getCommandParts()) - .containsExactly("/bin/sh", "-c", "gcloud beta emulators pubsub start --project my-project-id --host-port=0.0.0.0:8089"); + .containsExactly("/bin/sh", "-c", "gcloud beta emulators pubsub start --host-port 0.0.0.0:8085"); }); } diff --git a/embedded-google-storage/pom.xml b/embedded-google-storage/pom.xml index dc7a95b71..4d92a5259 100644 --- a/embedded-google-storage/pom.xml +++ b/embedded-google-storage/pom.xml @@ -16,6 +16,8 @@ 2.4.4 + + 0.3.0 @@ -27,6 +29,11 @@ com.playtika.testcontainers embedded-toxiproxy + + io.aiven + testcontainers-fake-gcs-server + ${testcontainers-fake-gcs-server.version} + com.google.cloud google-cloud-storage diff --git a/embedded-google-storage/src/main/java/com/playtika/testcontainer/storage/EmbeddedStorageBootstrapConfiguration.java b/embedded-google-storage/src/main/java/com/playtika/testcontainer/storage/EmbeddedStorageBootstrapConfiguration.java index 845b98b24..c137e0b07 100644 --- a/embedded-google-storage/src/main/java/com/playtika/testcontainer/storage/EmbeddedStorageBootstrapConfiguration.java +++ b/embedded-google-storage/src/main/java/com/playtika/testcontainer/storage/EmbeddedStorageBootstrapConfiguration.java @@ -6,6 +6,7 @@ import com.playtika.testcontainer.toxiproxy.ToxiproxyHelper; import com.playtika.testcontainer.toxiproxy.condition.ConditionalOnToxiProxyEnabled; import eu.rekawek.toxiproxy.ToxiproxyClient; +import io.aiven.testcontainers.fakegcsserver.FakeGcsServerContainer; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -18,17 +19,14 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.toxiproxy.ToxiproxyContainer; -import java.io.IOException; import java.util.LinkedHashMap; import java.util.Optional; import static com.playtika.testcontainer.common.utils.ContainerUtils.configureCommonsAndStart; import static com.playtika.testcontainer.storage.StorageProperties.BEAN_NAME_EMBEDDED_GOOGLE_STORAGE_SERVER; -import static java.lang.String.format; @Slf4j @Configuration @@ -45,7 +43,7 @@ public class EmbeddedStorageBootstrapConfiguration { @ConditionalOnToxiProxyEnabled(module = "google.storage") ToxiproxyClientProxy googleStorageContainerProxy(ToxiproxyClient toxiproxyClient, ToxiproxyContainer toxiproxyContainer, - @Qualifier(BEAN_NAME_EMBEDDED_GOOGLE_STORAGE_SERVER) GenericContainer storageServer, + @Qualifier(BEAN_NAME_EMBEDDED_GOOGLE_STORAGE_SERVER) FakeGcsServerContainer storageServer, ConfigurableEnvironment environment) { ToxiproxyClientProxy proxy = ToxiproxyHelper.createProxy( toxiproxyClient, @@ -60,46 +58,36 @@ ToxiproxyClientProxy googleStorageContainerProxy(ToxiproxyClient toxiproxyClient } @Bean(name = BEAN_NAME_EMBEDDED_GOOGLE_STORAGE_SERVER, destroyMethod = "stop") - GenericContainer storageServer(ConfigurableEnvironment environment, - StorageProperties properties, - Optional network) throws IOException { - - GenericContainer storageContainer = new GenericContainer<>(ContainerUtils.getDockerImageName(properties)) - .withExposedPorts(StorageProperties.PORT) - .withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint( - "/bin/fake-gcs-server", - "-backend", "memory", - "-scheme", "http", - "-host", "0.0.0.0", - "-port", String.valueOf(StorageProperties.PORT), - "-location", properties.getBucketLocation() - )) - .withNetworkAliases(GOOGLE_STORAGE_NETWORK_ALIAS); + FakeGcsServerContainer storageServer(ConfigurableEnvironment environment, + StorageProperties properties, + Optional network) { + FakeGcsServerContainer storageContainer = + new FakeGcsServerContainer(ContainerUtils.getDockerImageName(properties)) + .withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint( + "/bin/fake-gcs-server", + "-backend", "memory", + "-scheme", "http", + "-host", "0.0.0.0", + "-port", String.valueOf(StorageProperties.PORT), + "-location", properties.getBucketLocation() + )) + .withNetworkAliases(GOOGLE_STORAGE_NETWORK_ALIAS); network.ifPresent(storageContainer::withNetwork); - storageContainer = configureCommonsAndStart(storageContainer, properties, log); - prepareContainerConfiguration(storageContainer); + storageContainer = (FakeGcsServerContainer) configureCommonsAndStart(storageContainer, properties, log); registerStorageEnvironment(storageContainer, environment, properties); return storageContainer; } - private void prepareContainerConfiguration(GenericContainer container) throws IOException { - String containerEndpoint = buildContainerEndpoint(container); - - log.info("Google Cloud Fake Storage Server with externalUrl={}", containerEndpoint); - new GoogleCloudStorageHttpClient() - .sendUpdateConfigRequest(containerEndpoint); - } - private void registerStorageEnvironment( - GenericContainer container, + FakeGcsServerContainer container, ConfigurableEnvironment environment, StorageProperties properties) { LinkedHashMap map = new LinkedHashMap<>(); map.put("embedded.google.storage.host", container.getHost()); map.put("embedded.google.storage.port", container.getMappedPort(StorageProperties.PORT)); - map.put("embedded.google.storage.endpoint", buildContainerEndpoint(container)); + map.put("embedded.google.storage.endpoint", container.url()); map.put("embedded.google.storage.project-id", properties.getProjectId()); map.put("embedded.google.storage.bucket-location", properties.getBucketLocation()); map.put("embedded.google.storage.networkAlias", GOOGLE_STORAGE_NETWORK_ALIAS); @@ -118,11 +106,4 @@ StorageResourcesGenerator storageResourcesGenerator( StorageProperties storageProperties) { return new StorageResourcesGenerator(endpoint, storageProperties); } - - private String buildContainerEndpoint(GenericContainer container) { - return format( - "http://%s:%d", - container.getHost(), - container.getMappedPort(StorageProperties.PORT)); - } } diff --git a/embedded-google-storage/src/main/java/com/playtika/testcontainer/storage/GoogleCloudStorageHttpClient.java b/embedded-google-storage/src/main/java/com/playtika/testcontainer/storage/GoogleCloudStorageHttpClient.java deleted file mode 100644 index 67c09d071..000000000 --- a/embedded-google-storage/src/main/java/com/playtika/testcontainer/storage/GoogleCloudStorageHttpClient.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.playtika.testcontainer.storage; - -import lombok.extern.slf4j.Slf4j; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Scanner; - -@Slf4j -class GoogleCloudStorageHttpClient { - - //TODO: replace with HttpRequest from Java 11, when migrated - public void sendUpdateConfigRequest(String containerEndpoint) throws IOException { - HttpURLConnection connection = null; - try { - - String requestBody = "{" - + "\"externalUrl\": \"" + containerEndpoint + "\"" - + "}"; - - URL url = new URL(containerEndpoint + "/_internal/config"); - connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(5_000); - connection.setReadTimeout(5_000); - connection.setRequestMethod("PUT"); - connection.setDoOutput(true); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("Accept", "application/json"); - try (OutputStreamWriter osw = new OutputStreamWriter(connection.getOutputStream())) { - osw.write(requestBody); - osw.flush(); - } - - int responseCode = connection.getResponseCode(); - - if (responseCode != 200) { - String response = getResponseBody(connection); - log.error( - "error updating Google Cloud Fake Storage Server with external url, response status code {} != 200 message {}", - responseCode, - response); - } - } catch (Exception e) { - log.error("error updating Google Cloud Fake Storage Server with external host", e); - throw e; - } finally { - if (connection != null) { - connection.disconnect(); - } - } - } - - private String getResponseBody(HttpURLConnection connection) throws IOException { - try (InputStream inputStream = getStream(connection)) { - InputStreamReader streamReader = new InputStreamReader(inputStream); - Scanner s = new Scanner(streamReader).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; - } - } - - private InputStream getStream(HttpURLConnection connection) throws IOException { - InputStream errorStream = connection.getErrorStream(); - return errorStream != null ? errorStream : connection.getInputStream(); - } -} diff --git a/embedded-google-storage/src/test/java/com/playtika/testcontainer/storage/EmbeddedStorageBootstrapConfigurationTest.java b/embedded-google-storage/src/test/java/com/playtika/testcontainer/storage/EmbeddedStorageBootstrapConfigurationTest.java index 604b9bf6d..d77fd4c26 100644 --- a/embedded-google-storage/src/test/java/com/playtika/testcontainer/storage/EmbeddedStorageBootstrapConfigurationTest.java +++ b/embedded-google-storage/src/test/java/com/playtika/testcontainer/storage/EmbeddedStorageBootstrapConfigurationTest.java @@ -8,6 +8,7 @@ import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; import com.playtika.testcontainer.storage.EmbeddedStorageBootstrapConfigurationTest.TestConfig; +import io.aiven.testcontainers.fakegcsserver.FakeGcsServerContainer; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactoryUtils; @@ -21,7 +22,6 @@ import org.springframework.context.annotation.Import; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.test.context.ActiveProfiles; -import org.testcontainers.containers.GenericContainer; import java.io.IOException; import java.nio.ByteBuffer; @@ -118,9 +118,9 @@ public void shouldSetupDependsOnForStorage() { public void shouldHaveContainerWithExpectedDefaultProperties() { assertThat(beanFactory.getBean(BEAN_NAME_EMBEDDED_GOOGLE_STORAGE_SERVER)) .isNotNull() - .isInstanceOf(GenericContainer.class) - .satisfies(genericContainer -> { - GenericContainer container = (GenericContainer) genericContainer; + .isInstanceOf(FakeGcsServerContainer.class) + .satisfies(bean -> { + FakeGcsServerContainer container = (FakeGcsServerContainer) bean; assertThat(container.getExposedPorts()).containsExactly(4443); assertThat(container.getContainerInfo().getConfig().getEntrypoint()) diff --git a/embedded-grafana/README.adoc b/embedded-grafana/README.adoc index ea96ae1c6..1763c4ba9 100644 --- a/embedded-grafana/README.adoc +++ b/embedded-grafana/README.adoc @@ -16,12 +16,29 @@ * `embedded.grafana.enabled` `(true|false, default is true)` * `embedded.grafana.reuseContainer` `(true|false, default is false)` -* `embedded.grafana.dockerImage` `(default is 'grafana/grafana:13.0.1')` -** Image versions on https://hub.docker.com/r/grafana/grafana/tags[dockerhub] +* `embedded.grafana.dockerImage` `(default is 'grafana/otel-lgtm:0.9.3')` +** Image versions on https://hub.docker.com/r/grafana/otel-lgtm/tags[dockerhub] * `embedded.grafana.networkAlias` `(default is 'grafana')` * `embedded.grafana.username` `(default is 'admin')` * `embedded.grafana.password` `(default is 'password')` +* `embedded.grafana.port` `(default is 3000, Grafana UI)` +* `embedded.grafana.lokiPort` `(default is 3100, Loki HTTP)` +* `embedded.grafana.tempoPort` `(default is 3200, Tempo HTTP)` +* `embedded.grafana.otlpGrpcPort` `(default is 4317, OTLP gRPC)` +* `embedded.grafana.otlpHttpPort` `(default is 4318, OTLP HTTP)` +* `embedded.grafana.anonymousAuthEnabled` `(true|false, default is false)` - Sets `GF_AUTH_ANONYMOUS_ENABLED=true` +* `embedded.grafana.anonymousOrgRole` `(default is 'Viewer')` - Sets `GF_AUTH_ANONYMOUS_ORG_ROLE`; only applied when `anonymousAuthEnabled=true` * `embedded.toxiproxy.proxies.grafana.enabled` Enables both creation of the container with ToxiProxy TCP proxy and a proxy to the `embedded-grafana` container. +* `embedded.grafana.filesToInclude` Classpath resources to copy into the container at startup. Use this to provision custom Grafana datasources, dashboards, or alert rules. Example `bootstrap.yaml`: + +[source,yaml] +---- +embedded: + grafana: + filesToInclude: + - classpathResource: /embedded/grafana/provisioning/datasources/my-datasource.yml + containerPath: /etc/grafana/provisioning/datasources/my-datasource.yml +---- ==== Produces @@ -30,8 +47,12 @@ * `embedded.grafana.port` * `embedded.grafana.username` * `embedded.grafana.password` -* `embedded.grafana.toxiproxy.host` -* `embedded.grafana.toxiproxy.port` * `embedded.grafana.networkAlias` * `embedded.grafana.internalPort` +* `embedded.grafana.loki.port` +* `embedded.grafana.tempo.port` +* `embedded.grafana.otlp.grpc.port` +* `embedded.grafana.otlp.http.port` +* `embedded.grafana.toxiproxy.host` +* `embedded.grafana.toxiproxy.port` * Bean `ToxiproxyClientProxy grafanaContainerProxy` diff --git a/embedded-grafana/src/main/java/com/playtika/testcontainer/grafana/EmbeddedGrafanaBootstrapConfiguration.java b/embedded-grafana/src/main/java/com/playtika/testcontainer/grafana/EmbeddedGrafanaBootstrapConfiguration.java index a88a080d0..a1fe09d94 100644 --- a/embedded-grafana/src/main/java/com/playtika/testcontainer/grafana/EmbeddedGrafanaBootstrapConfiguration.java +++ b/embedded-grafana/src/main/java/com/playtika/testcontainer/grafana/EmbeddedGrafanaBootstrapConfiguration.java @@ -10,17 +10,14 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; -import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; -import org.testcontainers.containers.wait.strategy.WaitStrategy; +import org.testcontainers.grafana.LgtmStackContainer; import org.testcontainers.toxiproxy.ToxiproxyContainer; import java.util.LinkedHashMap; @@ -39,20 +36,11 @@ public class EmbeddedGrafanaBootstrapConfiguration { private static final String GRAFANA_NETWORK_ALIAS = "grafana.testcontainer.docker"; - @Bean - @ConditionalOnMissingBean(name = "grafanaWaitStrategy") - public WaitStrategy grafanaWaitStrategy(GrafanaProperties properties) { - return new HttpWaitStrategy() - .forPath("/") - .forPort(properties.getPort()) - .forStatusCode(200); - } - @Bean @ConditionalOnToxiProxyEnabled(module = "grafana") ToxiproxyClientProxy grafanaContainerProxy(ToxiproxyClient toxiproxyClient, ToxiproxyContainer toxiproxyContainer, - @Qualifier(GRAFANA_BEAN_NAME) GenericContainer grafana, + @Qualifier(GRAFANA_BEAN_NAME) LgtmStackContainer grafana, GrafanaProperties properties, ConfigurableEnvironment environment) { ToxiproxyClientProxy proxy = ToxiproxyHelper.createProxy( @@ -68,19 +56,21 @@ ToxiproxyClientProxy grafanaContainerProxy(ToxiproxyClient toxiproxyClient, } @Bean(name = GRAFANA_BEAN_NAME, destroyMethod = "stop") - public GenericContainer grafana(ConfigurableEnvironment environment, - GrafanaProperties properties, - WaitStrategy grafanaWaitStrategy, - Optional network) { + public LgtmStackContainer grafana(ConfigurableEnvironment environment, + GrafanaProperties properties, + Optional network) { - GenericContainer container = - new GenericContainer<>(ContainerUtils.getDockerImageName(properties)) + LgtmStackContainer container = + new LgtmStackContainer(ContainerUtils.getDockerImageName(properties)) .withEnv("GF_SECURITY_ADMIN_USER", properties.getUsername()) .withEnv("GF_SECURITY_ADMIN_PASSWORD", properties.getPassword()) - .withExposedPorts(properties.getPort()) .withNetwork(Network.SHARED) - .withNetworkAliases(properties.getNetworkAlias(), GRAFANA_NETWORK_ALIAS) - .waitingFor(grafanaWaitStrategy); + .withNetworkAliases(properties.getNetworkAlias(), GRAFANA_NETWORK_ALIAS); + + if (properties.isAnonymousAuthEnabled()) { + container.withEnv("GF_AUTH_ANONYMOUS_ENABLED", "true") + .withEnv("GF_AUTH_ANONYMOUS_ORG_ROLE", properties.getAnonymousOrgRole()); + } network.ifPresent(container::withNetwork); @@ -91,22 +81,25 @@ public GenericContainer grafana(ConfigurableEnvironment environment, return container; } - private void registerEnvironment(GenericContainer grafana, + private void registerEnvironment(LgtmStackContainer grafana, ConfigurableEnvironment environment, GrafanaProperties properties) { - Integer mappedPort = grafana.getMappedPort(properties.port); String host = grafana.getHost(); LinkedHashMap map = new LinkedHashMap<>(); map.put("embedded.grafana.host", host); - map.put("embedded.grafana.port", mappedPort); + map.put("embedded.grafana.port", grafana.getMappedPort(properties.getPort())); map.put("embedded.grafana.username", properties.getUsername()); map.put("embedded.grafana.password", properties.getPassword()); map.put("embedded.grafana.networkAlias", GRAFANA_NETWORK_ALIAS); map.put("embedded.grafana.internalPort", properties.getPort()); + map.put("embedded.grafana.loki.port", grafana.getMappedPort(properties.getLokiPort())); + map.put("embedded.grafana.tempo.port", grafana.getMappedPort(properties.getTempoPort())); + map.put("embedded.grafana.otlp.grpc.port", grafana.getMappedPort(properties.getOtlpGrpcPort())); + map.put("embedded.grafana.otlp.http.port", grafana.getMappedPort(properties.getOtlpHttpPort())); - log.info("Started Grafana server. Connection details: {}", map); + log.info("Started Grafana LGTM stack. Connection details: {}", map); MapPropertySource propertySource = new MapPropertySource("embeddedGrafanaInfo", map); environment.getPropertySources().addFirst(propertySource); diff --git a/embedded-grafana/src/main/java/com/playtika/testcontainer/grafana/GrafanaProperties.java b/embedded-grafana/src/main/java/com/playtika/testcontainer/grafana/GrafanaProperties.java index 214901245..ef3e6ea8d 100644 --- a/embedded-grafana/src/main/java/com/playtika/testcontainer/grafana/GrafanaProperties.java +++ b/embedded-grafana/src/main/java/com/playtika/testcontainer/grafana/GrafanaProperties.java @@ -17,12 +17,18 @@ public class GrafanaProperties extends CommonContainerProperties { String username = "admin"; String password = "password"; int port = 3000; + int lokiPort = 3100; + int tempoPort = 3200; + int otlpGrpcPort = 4317; + int otlpHttpPort = 4318; + boolean anonymousAuthEnabled = false; + String anonymousOrgRole = "Viewer"; - // https://hub.docker.com/r/grafana/grafana + // https://hub.docker.com/r/grafana/otel-lgtm @Override public String getDefaultDockerImage() { // Please don`t remove this comment. // renovate: datasource=docker - return "grafana/grafana:13.0.1"; + return "grafana/otel-lgtm:0.9.3"; } } diff --git a/embedded-grafana/src/test/java/com/playtika/testcontainer/grafana/EmbeddedGrafanaAnonymousAuthTest.java b/embedded-grafana/src/test/java/com/playtika/testcontainer/grafana/EmbeddedGrafanaAnonymousAuthTest.java new file mode 100644 index 000000000..3dd6f7f17 --- /dev/null +++ b/embedded-grafana/src/test/java/com/playtika/testcontainer/grafana/EmbeddedGrafanaAnonymousAuthTest.java @@ -0,0 +1,74 @@ +package com.playtika.testcontainer.grafana; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.util.UriComponentsBuilder; +import org.testcontainers.grafana.LgtmStackContainer; + +import java.io.IOException; + +import static com.playtika.testcontainer.grafana.GrafanaProperties.GRAFANA_BEAN_NAME; +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("anonymous") +@DirtiesContext +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = EmbeddedGrafanaAnonymousAuthTest.TestConfiguration.class) +class EmbeddedGrafanaAnonymousAuthTest { + + @Value("${embedded.grafana.host}") + private String grafanaHost; + @Value("${embedded.grafana.port}") + private int grafanaPort; + + @Autowired + @Qualifier(GRAFANA_BEAN_NAME) + private LgtmStackContainer grafanaContainer; + + @Test + void anonymousAuthEnvVarsShouldBeSetOnContainer() throws IOException, InterruptedException { + var result = grafanaContainer.execInContainer("sh", "-c", "env"); + assertThat(result.getStdout()) + .contains("GF_AUTH_ANONYMOUS_ENABLED=true") + .contains("GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer"); + } + + @Test + void grafanaApiShouldBeReachableWithoutCredentials() { + given() + .get(url("/api/health")) + .then() + .statusCode(200); + } + + @Test + void authenticatedEndpointShouldBeAccessibleWithoutCredentials() { + given() + .get(url("/api/dashboards/home")) + .then() + .statusCode(200); + } + + private String url(String path) { + return UriComponentsBuilder.newInstance() + .scheme("http") + .host(grafanaHost) + .port(grafanaPort) + .path(path) + .build() + .toUriString(); + } + + @EnableAutoConfiguration + @Configuration + static class TestConfiguration {} +} diff --git a/embedded-grafana/src/test/java/com/playtika/testcontainer/grafana/EmbeddedGrafanaBootstrapConfigurationTest.java b/embedded-grafana/src/test/java/com/playtika/testcontainer/grafana/EmbeddedGrafanaBootstrapConfigurationTest.java index 87392bbb4..8c32da890 100644 --- a/embedded-grafana/src/test/java/com/playtika/testcontainer/grafana/EmbeddedGrafanaBootstrapConfigurationTest.java +++ b/embedded-grafana/src/test/java/com/playtika/testcontainer/grafana/EmbeddedGrafanaBootstrapConfigurationTest.java @@ -6,21 +6,26 @@ import org.springframework.web.util.UriComponentsBuilder; import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.equalTo; class EmbeddedGrafanaBootstrapConfigurationTest extends BaseEmbeddedGrafanaTest { @Value("${embedded.grafana.username}") private String username; @Value("${embedded.grafana.password}") private String password; + @Value("${embedded.grafana.loki.port}") + private int lokiPort; + @Value("${embedded.grafana.tempo.port}") + private int tempoPort; + @Value("${embedded.grafana.otlp.http.port}") + private int otlpHttpPort; @Test - void shouldProvisionDatasourceFromConfigurationFile() { + void grafanaApiShouldBeReachable() { UriComponents uriComponents = UriComponentsBuilder.newInstance() .scheme("http") .host(grafanaHost) .port(grafanaPort) - .path("/api/datasources/name/Prometheus") + .path("/api/health") .build(); given() @@ -30,8 +35,38 @@ void shouldProvisionDatasourceFromConfigurationFile() { .get(uriComponents.toUriString()) .then() .assertThat() - .body("url", equalTo("http://prometheus:9090")) .statusCode(200); } -} \ No newline at end of file + @Test + void lokiShouldBeReachable() { + UriComponents uriComponents = UriComponentsBuilder.newInstance() + .scheme("http") + .host(grafanaHost) + .port(lokiPort) + .path("/ready") + .build(); + + given() + .get(uriComponents.toUriString()) + .then() + .assertThat() + .statusCode(200); + } + + @Test + void tempoShouldBeReachable() { + UriComponents uriComponents = UriComponentsBuilder.newInstance() + .scheme("http") + .host(grafanaHost) + .port(tempoPort) + .path("/ready") + .build(); + + given() + .get(uriComponents.toUriString()) + .then() + .assertThat() + .statusCode(200); + } +} diff --git a/embedded-grafana/src/test/resources/bootstrap-anonymous.yaml b/embedded-grafana/src/test/resources/bootstrap-anonymous.yaml new file mode 100644 index 000000000..c571c9968 --- /dev/null +++ b/embedded-grafana/src/test/resources/bootstrap-anonymous.yaml @@ -0,0 +1,3 @@ +embedded: + grafana: + anonymousAuthEnabled: true diff --git a/embedded-grafana/src/test/resources/bootstrap.yaml b/embedded-grafana/src/test/resources/bootstrap.yaml index 9c597cc98..47c9d9ac8 100644 --- a/embedded-grafana/src/test/resources/bootstrap.yaml +++ b/embedded-grafana/src/test/resources/bootstrap.yaml @@ -1,5 +1,3 @@ embedded: grafana: - filesToInclude: - - classpathResource: /embedded/grafana/provisioning/datasources/datasource.yml - containerPath: /etc/grafana/provisioning/datasources/datasource.yml \ No newline at end of file + enabled: true diff --git a/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/configuration/KafkaContainerConfiguration.java b/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/configuration/KafkaContainerConfiguration.java index 2c9845325..61b849bb8 100644 --- a/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/configuration/KafkaContainerConfiguration.java +++ b/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/configuration/KafkaContainerConfiguration.java @@ -1,5 +1,6 @@ package com.playtika.testcontainer.kafka.configuration; +import com.github.dockerjava.api.command.InspectContainerResponse; import com.playtika.testcontainer.common.utils.ContainerUtils; import com.playtika.testcontainer.kafka.KafkaTopicsConfigurer; import com.playtika.testcontainer.kafka.checks.KafkaStatusCheck; @@ -21,10 +22,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; -import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.Network; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.kafka.ConfluentKafkaContainer; import org.testcontainers.toxiproxy.ToxiproxyContainer; import org.testcontainers.utility.MountableFile; @@ -129,7 +130,7 @@ ToxiproxyClientProxy kafkaContainerSaslProxy(ToxiproxyClient toxiproxyClient, } @Bean(name = KAFKA_BEAN_NAME, destroyMethod = "stop") - public GenericContainer kafka( + public ConfluentKafkaContainer kafka( KafkaStatusCheck kafkaStatusCheck, KafkaConfigurationProperties kafkaProperties, ZookeeperConfigurationProperties zookeeperProperties, @@ -151,28 +152,32 @@ public GenericContainer kafka( // All properties: https://docs.confluent.io/platform/current/installation/configuration/ // Kafka Broker properties: https://docs.confluent.io/platform/current/installation/configuration/broker-configs.html - KafkaContainer kafka = new KafkaContainer(ContainerUtils.getDockerImageName(kafkaProperties)) { + ConfluentKafkaContainer kafka = new ConfluentKafkaContainer(ContainerUtils.getDockerImageName(kafkaProperties)) { @Override - public String getBootstrapServers() { - List servers = new ArrayList<>(); - servers.add("EXTERNAL_PLAINTEXT://" + getHost() + ":" + getMappedPort(kafkaExternalPort)); - servers.add("EXTERNAL_SASL_PLAINTEXT://" + getHost() + ":" + getMappedPort(saslPlaintextKafkaExternalPort)); - servers.add("INTERNAL_SASL_PLAINTEXT://" + KAFKA_HOST_NAME + ":" + saslPlaintextKafkaInternalPort); - servers.add("INTERNAL_PLAINTEXT://" + KAFKA_HOST_NAME + ":" + kafkaInternalPort); - - if (plainTextProxy != null) { - servers.add("TOXIPROXY_INTERNAL_PLAINTEXT://" + getHost() + ":" + plainTextProxy.getProxyPort()); - } - if (saslProxy != null) { - servers.add("TOXIPROXY_INTERNAL_SASL_PLAINTEXT://" + getHost() + ":" + saslProxy.getProxyPort()); - } - - return String.join(",", servers); + protected void containerIsStarting(InspectContainerResponse containerInfo) { + String hostname = containerInfo.getConfig().getHostName(); + List advertisedListeners = new ArrayList<>(); + advertisedListeners.add("EXTERNAL_PLAINTEXT://" + getHost() + ":" + getMappedPort(kafkaExternalPort)); + advertisedListeners.add("EXTERNAL_SASL_PLAINTEXT://" + getHost() + ":" + getMappedPort(saslPlaintextKafkaExternalPort)); + advertisedListeners.add("INTERNAL_PLAINTEXT://" + KAFKA_HOST_NAME + ":" + kafkaInternalPort); + advertisedListeners.add("INTERNAL_SASL_PLAINTEXT://" + KAFKA_HOST_NAME + ":" + saslPlaintextKafkaInternalPort); + advertisedListeners.add("TOXIPROXY_INTERNAL_PLAINTEXT://" + (plainTextProxy != null + ? getHost() + ":" + plainTextProxy.getProxyPort() + : KAFKA_HOST_NAME + ":" + toxiProxyKafkaInternalPort)); + advertisedListeners.add("TOXIPROXY_INTERNAL_SASL_PLAINTEXT://" + (saslProxy != null + ? getHost() + ":" + saslProxy.getProxyPort() + : KAFKA_HOST_NAME + ":" + toxiProxySaslPlaintextKafkaInternalPort)); + advertisedListeners.add("BROKER://" + hostname + ":9092"); + + String command = "#!/bin/bash\n" + + "export KAFKA_ADVERTISED_LISTENERS=" + String.join(",", advertisedListeners) + "\n" + + "/etc/confluent/docker/run\n"; + copyFileToContainer(Transferable.of(command, 0777), "/tmp/testcontainers_start.sh"); } } + .withCreateContainerCmdModifier(cmd -> cmd.withUser(kafkaProperties.getDockerUser())) .withCreateContainerCmdModifier(cmd -> cmd.withHostName(KAFKA_HOST_NAME)) - .withEmbeddedZookeeper() //see: https://stackoverflow.com/questions/41868161/kafka-in-kubernetes-cluster-how-to-publish-consume-messages-from-outside-of-kub //see: https://github.com/wurstmeister/kafka-docker/blob/develop/README.md // order matters: external then internal since kafka.client.ClientUtils.getPlaintextBrokerEndPoints take first for simple consumers @@ -182,6 +187,7 @@ public String getBootstrapServers() { "INTERNAL_SASL_PLAINTEXT:SASL_PLAINTEXT," + "INTERNAL_PLAINTEXT:PLAINTEXT," + "BROKER:PLAINTEXT," + + "CONTROLLER:PLAINTEXT," + "TOXIPROXY_INTERNAL_PLAINTEXT:PLAINTEXT," + "TOXIPROXY_INTERNAL_SASL_PLAINTEXT:SASL_PLAINTEXT" ) @@ -192,8 +198,10 @@ public String getBootstrapServers() { "INTERNAL_PLAINTEXT://0.0.0.0:" + kafkaInternalPort + "," + "TOXIPROXY_INTERNAL_PLAINTEXT://0.0.0.0:" + toxiProxyKafkaInternalPort + "," + "TOXIPROXY_INTERNAL_SASL_PLAINTEXT://0.0.0.0:" + toxiProxySaslPlaintextKafkaInternalPort + "," + - "BROKER://0.0.0.0:9092" + "BROKER://0.0.0.0:9092," + + "CONTROLLER://0.0.0.0:9099" ) + .withEnv("KAFKA_CONTROLLER_QUORUM_VOTERS", "1@localhost:9099") .withEnv("KAFKA_INTER_BROKER_LISTENER_NAME", "BROKER") .withEnv("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", "1") .withEnv("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", String.valueOf(kafkaProperties.getOffsetsTopicReplicationFactor())) @@ -217,12 +225,12 @@ public String getBootstrapServers() { kafkaFileSystemBind(kafkaProperties, kafka); zookeperFileSystemBind(zookeeperProperties, kafka); - kafka = (KafkaContainer) configureCommonsAndStart(kafka, kafkaProperties, log); + kafka = (ConfluentKafkaContainer) configureCommonsAndStart(kafka, kafkaProperties, log); registerKafkaEnvironment(kafka, environment, kafkaProperties); return kafka; } - private void kafkaFileSystemBind(KafkaConfigurationProperties kafkaProperties, KafkaContainer kafka) { + private void kafkaFileSystemBind(KafkaConfigurationProperties kafkaProperties, ConfluentKafkaContainer kafka) { KafkaConfigurationProperties.FileSystemBind fileSystemBind = kafkaProperties.getFileSystemBind(); if (fileSystemBind.isEnabled()) { String currentTimestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH-mm-ss-nnnnnnnnn")); @@ -231,11 +239,11 @@ private void kafkaFileSystemBind(KafkaConfigurationProperties kafkaProperties, K log.info("Writing kafka data to: {}", kafkaData); createPathAndParentOrMakeWritable(kafkaData); - kafka.addFileSystemBind(kafkaData.toString(), "/var/lib/kafka/data", BindMode.READ_WRITE); + kafka.withCopyToContainer(MountableFile.forHostPath(kafkaData.toString()), "/var/lib/kafka/data"); } } - private void zookeperFileSystemBind(ZookeeperConfigurationProperties zookeeperProperties, KafkaContainer kafka) { + private void zookeperFileSystemBind(ZookeeperConfigurationProperties zookeeperProperties, ConfluentKafkaContainer kafka) { ZookeeperConfigurationProperties.FileSystemBind zookeeperFileSystemBind = zookeeperProperties.getFileSystemBind(); if (zookeeperFileSystemBind.isEnabled()) { String currentTimestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH-mm-ss-nnnnnnnnn")); @@ -249,9 +257,9 @@ private void zookeperFileSystemBind(ZookeeperConfigurationProperties zookeeperPr log.info("Writing zookeeper transaction logs to: {}", zkTransactionLogs); createPathAndParentOrMakeWritable(zkData); - kafka.addFileSystemBind(zkData.toString(), "/var/lib/zookeeper/data", BindMode.READ_WRITE); + kafka.withCopyToContainer(MountableFile.forHostPath(zkData.toString()), "/var/lib/zookeeper/data"); createPathAndParentOrMakeWritable(zkTransactionLogs); - kafka.addFileSystemBind(zkTransactionLogs.toString(), "/var/lib/zookeeper/log", BindMode.READ_WRITE); + kafka.withCopyToContainer(MountableFile.forHostPath(zkTransactionLogs.toString()), "/var/lib/zookeeper/log"); } } diff --git a/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/properties/KafkaConfigurationProperties.java b/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/properties/KafkaConfigurationProperties.java index 760e25cab..ef4c1639d 100644 --- a/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/properties/KafkaConfigurationProperties.java +++ b/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/properties/KafkaConfigurationProperties.java @@ -10,9 +10,9 @@ import lombok.With; import org.springframework.boot.context.properties.ConfigurationProperties; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; @Data @EqualsAndHashCode(callSuper = true) @@ -64,7 +64,7 @@ public class KafkaConfigurationProperties extends CommonContainerProperties { protected FileSystemBind fileSystemBind = new FileSystemBind(); public KafkaConfigurationProperties() { - this.setCapabilities(Arrays.asList(Capability.NET_ADMIN)); + this.setCapabilities(List.of(Capability.NET_ADMIN)); } // https://hub.docker.com/r/confluentinc/cp-kafka diff --git a/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/properties/SchemaRegistryConfigurationProperties.java b/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/properties/SchemaRegistryConfigurationProperties.java index 56fdcc943..96544baac 100644 --- a/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/properties/SchemaRegistryConfigurationProperties.java +++ b/embedded-kafka/src/main/java/com/playtika/testcontainer/kafka/properties/SchemaRegistryConfigurationProperties.java @@ -6,7 +6,7 @@ import lombok.EqualsAndHashCode; import org.springframework.boot.context.properties.ConfigurationProperties; -import java.util.Arrays; +import java.util.List; import static com.playtika.testcontainer.kafka.properties.SchemaRegistryConfigurationProperties.Authentication.BASIC; import static com.playtika.testcontainer.kafka.properties.SchemaRegistryConfigurationProperties.Authentication.NONE; @@ -29,7 +29,7 @@ public class SchemaRegistryConfigurationProperties extends CommonContainerProper private Authentication authentication = NONE; public SchemaRegistryConfigurationProperties() { - this.setCapabilities(Arrays.asList(Capability.NET_ADMIN)); + this.setCapabilities(List.of(Capability.NET_ADMIN)); } public boolean isBasicAuthenticationEnabled() { diff --git a/embedded-keycloak/pom.xml b/embedded-keycloak/pom.xml index f8a364d94..6cc76fea0 100644 --- a/embedded-keycloak/pom.xml +++ b/embedded-keycloak/pom.xml @@ -14,7 +14,7 @@ embedded-keycloak - 26.6.2 + 3.3.0 @@ -26,14 +26,10 @@ com.playtika.testcontainers embedded-toxiproxy - - - org.keycloak - keycloak-core - ${keycloak.version} - provided - true + com.github.dasniko + testcontainers-keycloak + ${testcontainers-keycloak.version} diff --git a/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/EmbeddedKeycloakBootstrapConfiguration.java b/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/EmbeddedKeycloakBootstrapConfiguration.java index 8fe19da82..7645b5a78 100644 --- a/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/EmbeddedKeycloakBootstrapConfiguration.java +++ b/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/EmbeddedKeycloakBootstrapConfiguration.java @@ -1,9 +1,11 @@ package com.playtika.testcontainer.keycloak; import com.playtika.testcontainer.common.spring.DockerPresenceBootstrapConfiguration; +import com.playtika.testcontainer.common.utils.ContainerUtils; import com.playtika.testcontainer.toxiproxy.ToxiproxyClientProxy; import com.playtika.testcontainer.toxiproxy.ToxiproxyHelper; import com.playtika.testcontainer.toxiproxy.condition.ConditionalOnToxiProxyEnabled; +import dasniko.testcontainers.keycloak.KeycloakContainer; import eu.rekawek.toxiproxy.ToxiproxyClient; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; @@ -14,14 +16,18 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.testcontainers.containers.Network; import org.testcontainers.toxiproxy.ToxiproxyContainer; +import java.util.LinkedHashMap; import java.util.Optional; +import static com.playtika.testcontainer.common.utils.ContainerUtils.configureCommonsAndStart; import static com.playtika.testcontainer.keycloak.KeycloakProperties.BEAN_NAME_EMBEDDED_KEYCLOAK; -import static java.util.Objects.requireNonNull; +import static java.lang.String.format; @Slf4j @Configuration @@ -31,13 +37,16 @@ @ConditionalOnProperty(name = "embedded.keycloak.enabled", matchIfMissing = true) public class EmbeddedKeycloakBootstrapConfiguration { + private static final String KEYCLOAK_NETWORK_ALIAS = "keycloak.testcontainer.docker"; + private static final int KEYCLOAK_INTERNAL_HTTP_PORT = 8080; + @Bean @ConditionalOnToxiProxyEnabled(module = "keycloak") ToxiproxyClientProxy keycloakContainerProxy(ToxiproxyClient toxiproxyClient, - ToxiproxyContainer toxiproxyContainer, - @Qualifier(BEAN_NAME_EMBEDDED_KEYCLOAK) KeycloakContainer keycloakContainer, - KeycloakProperties properties, - ConfigurableEnvironment environment) { + ToxiproxyContainer toxiproxyContainer, + @Qualifier(BEAN_NAME_EMBEDDED_KEYCLOAK) KeycloakContainer keycloakContainer, + KeycloakProperties properties, + ConfigurableEnvironment environment) { ToxiproxyClientProxy proxy = ToxiproxyHelper.createProxy( toxiproxyClient, toxiproxyContainer, @@ -50,26 +59,105 @@ ToxiproxyClientProxy keycloakContainerProxy(ToxiproxyClient toxiproxyClient, return proxy; } - @Bean - public KeycloakContainerFactory keycloakContainerFactory(ConfigurableEnvironment environment, - KeycloakProperties properties, - ResourceLoader resourceLoader, - Optional network) { - return new KeycloakContainerFactory(environment, properties, resourceLoader, network); + @Bean(name = BEAN_NAME_EMBEDDED_KEYCLOAK, destroyMethod = "stop") + public KeycloakContainer keycloakContainer(ConfigurableEnvironment environment, + KeycloakProperties properties, + ResourceLoader resourceLoader, + Optional network) { + KeycloakContainer keycloak = new KeycloakContainer(ContainerUtils.getDockerImageName(properties).toString()) + .withNetworkAliases(KEYCLOAK_NETWORK_ALIAS) + .withAdminUsername(properties.getAdminUser()) + .withAdminPassword(properties.getAdminPassword()); + + applyDbConfig(keycloak, properties); + applyImportFile(keycloak, properties, resourceLoader); + + network.ifPresent(keycloak::withNetwork); + + keycloak = (KeycloakContainer) configureCommonsAndStart(keycloak, properties, log); + registerEnvironment(keycloak, environment, properties); + return keycloak; } - /** - * Creates and starts a {@link KeycloakContainer} if property {@code embedded.keycloak.enabled} - * evaluates to {@code true}. The configuration makes no difference if just vanilla Keycloak is - * on the classpath or any Spring adapter. The container will always be needed. Also registers a - * shutdown hook to stop the container on context shutdown. - * - * @param factory The {@link KeycloakContainerFactory} to use, injected by Spring, must not be - * null - * @return The created {@link KeycloakContainer} instance to be registered as bean - */ - @Bean(name = BEAN_NAME_EMBEDDED_KEYCLOAK, destroyMethod = "stop") - public KeycloakContainer keycloakContainer(KeycloakContainerFactory factory) { - return requireNonNull(factory).newKeycloakContainer(); + private void applyDbConfig(KeycloakContainer keycloak, KeycloakProperties properties) { + if (properties.getDbVendor() != null) { + keycloak.withEnv("DB", properties.getDbVendor()); + } + if (properties.getDbAddr() != null) { + keycloak.withEnv("DB_URL_HOST", properties.getDbAddr()); + } + if (properties.getDbPort() != null) { + keycloak.withEnv("DB_URL_PORT", properties.getDbPort()); + } + if (properties.getDbDatabase() != null) { + keycloak.withEnv("DB_URL_DATABASE", properties.getDbDatabase()); + } + if (properties.getDbSchema() != null) { + keycloak.withEnv("DB_SCHEMA", properties.getDbSchema()); + } + if (properties.getDbUser() != null) { + keycloak.withEnv("DB_USERNAME", properties.getDbUser()); + } + if (properties.getDbUserFile() != null) { + keycloak.withEnv("DB_USER_FILE", properties.getDbUserFile()); + } + if (properties.getDbPassword() != null) { + keycloak.withEnv("DB_PASSWORD", properties.getDbPassword()); + } + if (properties.getDbPasswordFile() != null) { + keycloak.withEnv("DB_PASSWORD_FILE", properties.getDbPasswordFile()); + } + } + + private void applyImportFile(KeycloakContainer keycloak, KeycloakProperties properties, ResourceLoader resourceLoader) { + String importFile = properties.getImportFile(); + if (importFile == null) { + return; + } + checkImportFileExists(resourceLoader, importFile); + keycloak.withRealmImportFile(importFile); + } + + private void checkImportFileExists(ResourceLoader resourceLoader, String importFile) { + Resource resource = resourceLoader.getResource("classpath:" + importFile); + if (!resource.exists()) { + throw new ImportFileNotFoundException(importFile); + } + log.debug("Using import file: {}", resource.getFilename()); + } + + private void registerEnvironment(KeycloakContainer keycloak, + ConfigurableEnvironment environment, + KeycloakProperties properties) { + String host = keycloak.getHost(); + Integer httpPort = keycloak.getHttpPort(); + String authServerUrl = buildAuthServerUrl(keycloak, properties); + + LinkedHashMap map = new LinkedHashMap<>(); + map.put("embedded.keycloak.host", host); + map.put("embedded.keycloak.http-port", httpPort); + map.put("embedded.keycloak.auth-server-url", authServerUrl); + map.put("embedded.keycloak.networkAlias", KEYCLOAK_NETWORK_ALIAS); + map.put("embedded.keycloak.internalPort", KEYCLOAK_INTERNAL_HTTP_PORT); + + log.info("Started Keycloak server. Connection details: {}", map); + + MapPropertySource propertySource = new MapPropertySource("embeddedKeycloakInfo", map); + environment.getPropertySources().addFirst(propertySource); + } + + private String buildAuthServerUrl(KeycloakContainer keycloak, KeycloakProperties properties) { + return format("http://%s:%d%s", keycloak.getHost(), keycloak.getHttpPort(), properties.getAuthBasePath()); + } + + public static final class ImportFileNotFoundException extends IllegalArgumentException { + + private static final long serialVersionUID = 6350884396691857560L; + + ImportFileNotFoundException(String importFile) { + super(format( + "Classpath resource '%s' defined through 'embedded.keycloak.import-file' does not exist.", + importFile)); + } } } diff --git a/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/KeycloakContainer.java b/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/KeycloakContainer.java deleted file mode 100644 index efe3f2c30..000000000 --- a/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/KeycloakContainer.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.playtika.testcontainer.keycloak; - -import com.playtika.testcontainer.common.utils.ContainerUtils; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.containers.wait.strategy.WaitStrategy; -import org.testcontainers.utility.MountableFile; - -import static java.lang.String.format; - -@Slf4j -public class KeycloakContainer extends GenericContainer { - - public static final int KEYCLOAK_DEFAULT_HTTP_PORT_INTERNAL = 8080; - - private final KeycloakProperties properties; - private final ResourceLoader resourceLoader; - - public KeycloakContainer(KeycloakProperties properties, - ResourceLoader resourceLoader) { - super(ContainerUtils.getDockerImageName(properties)); - this.properties = properties; - this.resourceLoader = resourceLoader; - } - - @Override - protected void configure() { - withEnv("HTTP_ENABLED", String.valueOf(true)); - withEnv("HTTP_PORT", String.valueOf(KEYCLOAK_DEFAULT_HTTP_PORT_INTERNAL)); - withExposedPorts(KEYCLOAK_DEFAULT_HTTP_PORT_INTERNAL); - withEnv("KEYCLOAK_ADMIN", properties.getAdminUser()); - withEnv("KEYCLOAK_ADMIN_PASSWORD", properties.getAdminPassword()); - withDB(); - withCommand(properties.getCommand()); - waitingFor(waitForListeningPort()); - withImportFile(properties.getImportFile()); - } - - private void withDB() { - withDBVendor(); - withDBAddr(); - withDBPort(); - withDBDatabase(); - withDBSchema(); - withDBUser(); - withDBUserFile(); - withDBPassword(); - withDBPasswordFile(); - } - - private void withDBVendor() { - String dbVendor = properties.getDbVendor(); - if (dbVendor != null) { - withEnv("DB", dbVendor); - } - } - - private void withDBAddr() { - String dbAddr = properties.getDbAddr(); - if (dbAddr != null) { - withEnv("DB_URL_HOST", dbAddr); - } - } - - private void withDBPort() { - String dbPort = properties.getDbPort(); - if (dbPort != null) { - withEnv("DB_URL_PORT", dbPort); - } - } - - private void withDBDatabase() { - String dbDatabase = properties.getDbDatabase(); - if (dbDatabase != null) { - withEnv("DB_URL_DATABASE", dbDatabase); - } - } - - private void withDBSchema() { - String dbSchema = properties.getDbSchema(); - if (dbSchema != null) { - withEnv("DB_SCHEMA", dbSchema); - } - } - - private void withDBUser() { - String dbUser = properties.getDbUser(); - if (dbUser != null) { - withEnv("DB_USERNAME", dbUser); - } - } - - private void withDBUserFile() { - String dbUserFile = properties.getDbUserFile(); - if (dbUserFile != null) { - withEnv("DB_USER_FILE", dbUserFile); - } - } - - private void withDBPassword() { - String dbPassword = properties.getDbPassword(); - if (dbPassword != null) { - withEnv("DB_PASSWORD", dbPassword); - } - } - - private void withDBPasswordFile() { - String dbPasswordFile = properties.getDbPasswordFile(); - if (dbPasswordFile != null) { - withEnv("DB_PASSWORD_FILE", dbPasswordFile); - } - } - - private void withImportFile(String importFile) { - if (importFile == null) { - return; - } - - checkExists(importFile); - - String importFileInContainer = "/opt/keycloak/data/import/" + importFile; - withCopyFileToContainer( - MountableFile.forClasspathResource(importFile), - importFileInContainer - ); - } - - private void checkExists(String importFile) { - Resource resource = resourceLoader.getResource("classpath:" + importFile); - if (resource.exists()) { - log.debug("Using import file: {}", resource.getFilename()); - return; - } - - throw new ImportFileNotFoundException(importFile); - } - - private WaitStrategy waitForListeningPort() { - return Wait - .forListeningPort() - .withStartupTimeout(properties.getTimeoutDuration()); - } - - public String getIp() { - return getContainerIpAddress(); - } - - public Integer getHttpPort() { - return getMappedPort(KEYCLOAK_DEFAULT_HTTP_PORT_INTERNAL); - } - - public String getAuthServerUrl() { - return format("http://%s:%d%s", getIp(), getHttpPort(), properties.getAuthBasePath()); - } - - public static final class ImportFileNotFoundException extends IllegalArgumentException { - - private static final long serialVersionUID = 6350884396691857560L; - - ImportFileNotFoundException(String importFile) { - super(format( - "Classpath resource '%s' defined through 'embedded.keycloak.import-file' does not exist.", - importFile)); - } - } -} diff --git a/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/KeycloakContainerFactory.java b/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/KeycloakContainerFactory.java deleted file mode 100644 index 04929e815..000000000 --- a/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/KeycloakContainerFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.playtika.testcontainer.keycloak; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.io.ResourceLoader; -import org.testcontainers.containers.Network; - -import java.util.LinkedHashMap; -import java.util.Optional; - -import static com.playtika.testcontainer.common.utils.ContainerUtils.configureCommonsAndStart; -import static com.playtika.testcontainer.keycloak.KeycloakContainer.KEYCLOAK_DEFAULT_HTTP_PORT_INTERNAL; - -@Slf4j -@RequiredArgsConstructor -public class KeycloakContainerFactory { - - private static final String KEYCLOAK_NETWORK_ALIAS = "keycloak.testcontainer.docker"; - - private final ConfigurableEnvironment environment; - private final KeycloakProperties properties; - private final ResourceLoader resourceLoader; - private final Optional network; - - public KeycloakContainer newKeycloakContainer() { - KeycloakContainer keycloak = new KeycloakContainer(properties, resourceLoader) - .withNetworkAliases(KEYCLOAK_NETWORK_ALIAS); - - network.ifPresent(keycloak::withNetwork); - - keycloak = (KeycloakContainer) configureCommonsAndStart(keycloak, properties, log); - registerKeycloakEnvironment(keycloak); - return keycloak; - } - - private void registerKeycloakEnvironment(KeycloakContainer keycloak) { - LinkedHashMap map = new LinkedHashMap<>(); - map.put("embedded.keycloak.host", keycloak.getIp()); - map.put("embedded.keycloak.http-port", keycloak.getHttpPort()); - map.put("embedded.keycloak.auth-server-url", keycloak.getAuthServerUrl()); - map.put("embedded.keycloak.networkAlias", KEYCLOAK_NETWORK_ALIAS); - map.put("embedded.keycloak.internalPort", KEYCLOAK_DEFAULT_HTTP_PORT_INTERNAL); - - log.info("Started Keycloak server. Connection details: {}", map); - - MapPropertySource propertySource = new MapPropertySource("embeddedKeycloakInfo", map); - environment.getPropertySources().addFirst(propertySource); - } -} diff --git a/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/KeycloakProperties.java b/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/KeycloakProperties.java index d3b019848..283cb15be 100644 --- a/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/KeycloakProperties.java +++ b/embedded-keycloak/src/main/java/com/playtika/testcontainer/keycloak/KeycloakProperties.java @@ -12,18 +12,12 @@ public class KeycloakProperties extends CommonContainerProperties { public static final String BEAN_NAME_EMBEDDED_KEYCLOAK = "embeddedKeycloak"; - public static final String[] DEFAULT_COMMAND = {"start-dev", "--import-realm"}; - public static final String DEFAULT_ADMIN_USER = "admin"; public static final String DEFAULT_ADMIN_PASSWORD = "letmein"; public static final String DEFAULT_REALM = "master"; public static final String DEFAULT_AUTH_BASE_PATH = "/"; public static final String DEFAULT_DB_VENDOR = "dev-mem"; - /** - * The command string issued to the container. - */ - private String[] command = DEFAULT_COMMAND; /** * The admin username to use. */ @@ -77,6 +71,11 @@ public class KeycloakProperties extends CommonContainerProperties { */ private String dbPasswordFile; + @Override + public String[] getCommand() { + return null; + } + @Override public String getDefaultDockerImage() { // Please don`t remove this comment. diff --git a/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/spring/EmbeddedKeycloakBootstrapConfigurationTest.java b/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/spring/EmbeddedKeycloakBootstrapConfigurationTest.java index 9baf51d78..43dd5f294 100644 --- a/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/spring/EmbeddedKeycloakBootstrapConfigurationTest.java +++ b/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/spring/EmbeddedKeycloakBootstrapConfigurationTest.java @@ -1,7 +1,7 @@ package com.playtika.testcontainer.keycloak.spring; -import com.playtika.testcontainer.keycloak.KeycloakContainer; import com.playtika.testcontainer.keycloak.util.KeyCloakToken; +import dasniko.testcontainers.keycloak.KeycloakContainer; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -51,7 +51,7 @@ public void propertiesAreAvailable() { keycloakContainer.getHttpPort())); assertThat(environment.getProperty("embedded.keycloak.host")) - .isEqualTo(keycloakContainer.getIp()); + .isEqualTo(keycloakContainer.getHost()); assertThat(environment.getProperty("embedded.keycloak.http-port", Integer.class)) .isEqualTo(keycloakContainer.getHttpPort()); diff --git a/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/util/KeycloakJwtAuthenticationConverter.java b/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/util/KeycloakJwtAuthenticationConverter.java index 43d9ef967..d612aab10 100644 --- a/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/util/KeycloakJwtAuthenticationConverter.java +++ b/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/util/KeycloakJwtAuthenticationConverter.java @@ -37,7 +37,7 @@ private Set extractResourceRoles(Jwt jwt) { var resourceRoles = new ArrayList(); if (resourceAccess.containsKey(clientId)) { - var resource = (Map>) resourceAccess.get(clientId); + var resource = resourceAccess.get(clientId); resourceRoles.addAll(resource.get("roles")); } diff --git a/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/vanilla/KeycloakContainerTest.java b/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/vanilla/KeycloakContainerTest.java index 1802b9b30..e7554b8a1 100644 --- a/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/vanilla/KeycloakContainerTest.java +++ b/embedded-keycloak/src/test/java/com/playtika/testcontainer/keycloak/vanilla/KeycloakContainerTest.java @@ -1,50 +1,26 @@ package com.playtika.testcontainer.keycloak.vanilla; -import com.playtika.testcontainer.keycloak.KeycloakContainer; -import com.playtika.testcontainer.keycloak.KeycloakContainer.ImportFileNotFoundException; -import com.playtika.testcontainer.keycloak.KeycloakProperties; +import com.playtika.testcontainer.keycloak.EmbeddedKeycloakBootstrapConfiguration; +import com.playtika.testcontainer.keycloak.EmbeddedKeycloakBootstrapConfiguration.ImportFileNotFoundException; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.testcontainers.containers.ContainerLaunchException; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThat; -@ExtendWith(MockitoExtension.class) public class KeycloakContainerTest { - @Mock - ResourceLoader resourceLoader; - - @Spy - KeycloakProperties properties = setupProperties(); - - @InjectMocks - KeycloakContainer container; - + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EmbeddedKeycloakBootstrapConfiguration.class)); @Test public void shouldThrowImportFileNotFoundExceptionWhenImportFileDoesNotExist() { - Resource importFile = mock(Resource.class); - when(importFile.exists()).thenReturn(false); - when(resourceLoader.getResource(any())).thenReturn(importFile); - - assertThatThrownBy(container::start) - .isInstanceOf(ContainerLaunchException.class) - .hasCauseInstanceOf(ImportFileNotFoundException.class); - } - - private KeycloakProperties setupProperties() { - KeycloakProperties properties = new KeycloakProperties(); - properties.setImportFile("any-import-file.json"); - return properties; + contextRunner + .withPropertyValues("embedded.keycloak.import-file=non-existent-file.json") + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .hasRootCauseInstanceOf(ImportFileNotFoundException.class); + }); } } diff --git a/embedded-keydb/src/main/java/com/playtika/testcontainer/keydb/wait/KeyDbClusterStatusCheck.java b/embedded-keydb/src/main/java/com/playtika/testcontainer/keydb/wait/KeyDbClusterStatusCheck.java index 2d57fb3da..1b394872b 100644 --- a/embedded-keydb/src/main/java/com/playtika/testcontainer/keydb/wait/KeyDbClusterStatusCheck.java +++ b/embedded-keydb/src/main/java/com/playtika/testcontainer/keydb/wait/KeyDbClusterStatusCheck.java @@ -39,11 +39,16 @@ private void logClusterInfo() { String info = jedis.info(); Map config = jedis.configGet("*"); String clusterNodes = jedis.clusterNodes(); - log.error("Cluster in failed state:\n" + - "-- cluster info:\n{}\n" + - "-- nodes:\n{}\n" + - "-- info:\n{}\n" + - "-- config:\n{}", + log.error(""" + Cluster in failed state: + -- cluster info: + {} + -- nodes: + {} + -- info: + {} + -- config: + {}""", clusterInfo, clusterNodes, info, String.join("\n", config.values())); } } diff --git a/embedded-memsql/pom.xml b/embedded-memsql/pom.xml index 3aae826fd..c32dc1cd9 100644 --- a/embedded-memsql/pom.xml +++ b/embedded-memsql/pom.xml @@ -23,6 +23,10 @@ com.playtika.testcontainers embedded-toxiproxy + + org.testcontainers + testcontainers-jdbc + org.mariadb.jdbc mariadb-java-client diff --git a/embedded-memsql/src/main/java/com/playtika/testcontainer/memsql/EmbeddedMemSqlBootstrapConfiguration.java b/embedded-memsql/src/main/java/com/playtika/testcontainer/memsql/EmbeddedMemSqlBootstrapConfiguration.java index d173eed62..27315ca61 100644 --- a/embedded-memsql/src/main/java/com/playtika/testcontainer/memsql/EmbeddedMemSqlBootstrapConfiguration.java +++ b/embedded-memsql/src/main/java/com/playtika/testcontainer/memsql/EmbeddedMemSqlBootstrapConfiguration.java @@ -17,7 +17,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.toxiproxy.ToxiproxyContainer; import org.testcontainers.utility.MountableFile; @@ -48,7 +47,7 @@ MemSqlStatusCheck memSqlStartupCheckStrategy(MemSqlProperties properties) { @ConditionalOnToxiProxyEnabled(module = "memsql") ToxiproxyClientProxy memsqlContainerProxy(ToxiproxyClient toxiproxyClient, ToxiproxyContainer toxiproxyContainer, - @Qualifier(BEAN_NAME_EMBEDDED_MEMSQL) GenericContainer memsql, + @Qualifier(BEAN_NAME_EMBEDDED_MEMSQL) MemSqlContainer memsql, MemSqlProperties properties, ConfigurableEnvironment environment) { ToxiproxyClientProxy proxy = ToxiproxyHelper.createProxy( @@ -64,44 +63,41 @@ ToxiproxyClientProxy memsqlContainerProxy(ToxiproxyClient toxiproxyClient, } @Bean(name = BEAN_NAME_EMBEDDED_MEMSQL, destroyMethod = "stop") - public GenericContainer memsql(ConfigurableEnvironment environment, - MemSqlProperties properties, - MemSqlStatusCheck memSqlStatusCheck, - Optional network) { - GenericContainer memsql = new GenericContainer<>(ContainerUtils.getDockerImageName(properties)) - .withEnv("IGNORE_MIN_REQUIREMENTS", "1") - .withEnv("SINGLESTORE_SET_GLOBAL_DEFAULT_PARTITIONS_PER_LEAF", "1") - .withEnv("LICENSE_KEY", properties.getLicenseKey()) - .withEnv("SINGLESTORE_LICENSE", properties.getLicenseKey()) - .withEnv("ROOT_PASSWORD", properties.getPassword()) - .withEnv("START_AFTER_INIT", "Y") - .withExposedPorts(properties.port) + public MemSqlContainer memsql(ConfigurableEnvironment environment, + MemSqlProperties properties, + MemSqlStatusCheck memSqlStatusCheck, + Optional network) { + MemSqlContainer memsql = new MemSqlContainer(ContainerUtils.getDockerImageName(properties)) + .withDatabaseName(properties.getDatabase()) + .withUsername(properties.getUser()) + .withPassword(properties.getPassword()) + .withLicenseKey(properties.getLicenseKey()) .withCopyFileToContainer(MountableFile.forClasspathResource("mem.sql"), "/schema.sql") .waitingFor(memSqlStatusCheck) .withNetworkAliases(MEMSQL_NETWORK_ALIAS); - if ("aarch".equals(System.getProperty("system.arch"))){ + + if ("aarch".equals(System.getProperty("system.arch"))) { memsql = memsql.withCommand("platform", "linux/amd64"); } + network.ifPresent(memsql::withNetwork); - memsql = configureCommonsAndStart(memsql, properties, log); + memsql = (MemSqlContainer) configureCommonsAndStart(memsql, properties, log); registerMemSqlEnvironment(memsql, environment, properties); return memsql; } - private void registerMemSqlEnvironment(GenericContainer memsql, - ConfigurableEnvironment environment, - MemSqlProperties properties) { - Integer mappedPort = memsql.getMappedPort(properties.port); - String host = memsql.getHost(); - + private void registerMemSqlEnvironment(MemSqlContainer memsql, + ConfigurableEnvironment environment, + MemSqlProperties properties) { LinkedHashMap map = new LinkedHashMap<>(); - map.put("embedded.memsql.port", mappedPort); - map.put("embedded.memsql.host", host); - map.put("embedded.memsql.schema", properties.getDatabase()); - map.put("embedded.memsql.user", properties.getUser()); - map.put("embedded.memsql.password", properties.getPassword()); + map.put("embedded.memsql.port", memsql.getMappedPort(MemSqlContainer.MEMSQL_PORT)); + map.put("embedded.memsql.host", memsql.getHost()); + map.put("embedded.memsql.schema", memsql.getDatabaseName()); + map.put("embedded.memsql.user", memsql.getUsername()); + map.put("embedded.memsql.password", memsql.getPassword()); + map.put("embedded.memsql.jdbcUrl", memsql.getJdbcUrl()); map.put("embedded.memsql.networkAlias", MEMSQL_NETWORK_ALIAS); - map.put("embedded.memsql.internalPort", properties.getPort()); + map.put("embedded.memsql.internalPort", MemSqlContainer.MEMSQL_PORT); log.info("Started memsql server. Connection details {} ", map); diff --git a/embedded-memsql/src/main/java/com/playtika/testcontainer/memsql/MemSqlContainer.java b/embedded-memsql/src/main/java/com/playtika/testcontainer/memsql/MemSqlContainer.java new file mode 100644 index 000000000..8886b6a4b --- /dev/null +++ b/embedded-memsql/src/main/java/com/playtika/testcontainer/memsql/MemSqlContainer.java @@ -0,0 +1,144 @@ +package com.playtika.testcontainer.memsql; + +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.utility.DockerImageName; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class MemSqlContainer extends JdbcDatabaseContainer { + + private static final DockerImageName DEFAULT_IMAGE_NAME = + DockerImageName.parse("ghcr.io/singlestore-labs/singlestoredb-dev"); + + static final int MEMSQL_PORT = 3306; + + private String database = "test_db"; + private String username = "root"; + private String password = "pass"; + private String licenseKey = ""; + + public MemSqlContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + } + + public MemSqlContainer withDatabaseName(String database) { + this.database = database; + return self(); + } + + public MemSqlContainer withUsername(String username) { + this.username = username; + return self(); + } + + @Override + public MemSqlContainer withPassword(String password) { + this.password = password; + return self(); + } + + public MemSqlContainer withLicenseKey(String licenseKey) { + this.licenseKey = licenseKey; + return self(); + } + + @Override + protected void configure() { + addEnv("IGNORE_MIN_REQUIREMENTS", "1"); + addEnv("SINGLESTORE_SET_GLOBAL_DEFAULT_PARTITIONS_PER_LEAF", "1"); + addEnv("LICENSE_KEY", licenseKey); + addEnv("SINGLESTORE_LICENSE", licenseKey); + addEnv("ROOT_PASSWORD", password); + addEnv("START_AFTER_INIT", "Y"); + addExposedPort(MEMSQL_PORT); + } + + @Override + public String getDriverClassName() { + return "org.mariadb.jdbc.Driver"; + } + + @Override + public String getJdbcUrl() { + return "jdbc:mariadb://" + getHost() + ":" + getMappedPort(MEMSQL_PORT) + "/" + database; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getDatabaseName() { + return database; + } + + @Override + protected String getTestQueryString() { + return "SELECT 1"; + } + + @Override + protected void waitUntilContainerStarted() { + // JdbcDatabaseContainer.waitUntilContainerStarted() does not call super, so the + // waitingFor() strategy is never executed and the JDBC check connects to getJdbcUrl() + // which includes the database name — failing with "Unknown database" since the DB + // doesn't exist yet. Connect to the root URL first, create the DB, then hand off. + String rootUrl = "jdbc:mariadb://" + getHost() + ":" + getMappedPort(MEMSQL_PORT) + "/"; + long startNanos = System.nanoTime(); + long timeoutNanos = TimeUnit.SECONDS.toNanos(getStartupTimeoutSeconds()); + Exception lastException = null; + + while ((System.nanoTime() - startNanos) < timeoutNanos) { + if (!isRunning()) { + sleep(500); + continue; + } + try (Connection conn = DriverManager.getConnection(rootUrl, username, password); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE DATABASE IF NOT EXISTS " + database); + log.debug("Database '{}' created/verified", database); + break; + } catch (Exception e) { + lastException = e; + sleep(500); + } + } + + if (lastException != null && !databaseExists(rootUrl)) { + throw new ContainerLaunchException("Could not create database '" + database + "'", lastException); + } + + super.waitUntilContainerStarted(); + } + + private boolean databaseExists(String rootUrl) { + try (Connection conn = DriverManager.getConnection(rootUrl, username, password); + Statement stmt = conn.createStatement()) { + stmt.execute("USE " + database); + return true; + } catch (Exception e) { + return false; + } + } + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/embedded-memsql/src/main/java/com/playtika/testcontainer/memsql/MemSqlProperties.java b/embedded-memsql/src/main/java/com/playtika/testcontainer/memsql/MemSqlProperties.java index 080ec7c3a..059adb896 100644 --- a/embedded-memsql/src/main/java/com/playtika/testcontainer/memsql/MemSqlProperties.java +++ b/embedded-memsql/src/main/java/com/playtika/testcontainer/memsql/MemSqlProperties.java @@ -23,7 +23,7 @@ public class MemSqlProperties extends CommonContainerProperties { @NotEmpty String licenseKey; int port = 3306; - String statusCheck = "source /schema.sql; use test_db; SELECT 1;"; + String statusCheck = "SELECT 1;"; @Override public String getDefaultDockerImage() { diff --git a/embedded-memsql/src/test/java/com/playtika/testcontainer/memsql/EmbeddedMemSqlBootstrapConfigurationTest.java b/embedded-memsql/src/test/java/com/playtika/testcontainer/memsql/EmbeddedMemSqlBootstrapConfigurationTest.java index 6ff2093c4..51fc5bb6c 100644 --- a/embedded-memsql/src/test/java/com/playtika/testcontainer/memsql/EmbeddedMemSqlBootstrapConfigurationTest.java +++ b/embedded-memsql/src/test/java/com/playtika/testcontainer/memsql/EmbeddedMemSqlBootstrapConfigurationTest.java @@ -1,6 +1,9 @@ package com.playtika.testcontainer.memsql; +import com.playtika.testcontainer.toxiproxy.ToxiproxyClientProxy; +import eu.rekawek.toxiproxy.model.ToxicDirection; import lombok.extern.slf4j.Slf4j; +import org.assertj.core.data.Offset; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -35,8 +38,8 @@ public class EmbeddedMemSqlBootstrapConfigurationTest { @Autowired ConfigurableEnvironment environment; -// @Autowired -// NetworkTestOperations memsqlNetworkTestOperations; + @Autowired + ToxiproxyClientProxy memsqlContainerProxy; @Test public void shouldConnectToMemSQL() throws Exception { @@ -46,20 +49,21 @@ public void shouldConnectToMemSQL() throws Exception { assertThat(jdbcTemplate.queryForList("select * from foo")).hasSize(3); } -// @Test -// @Disabled("image doesn't support to simply install tc") -// public void shouldEmulateLatency() throws Exception { -// jdbcTemplate.execute("create table bar (id int primary key);"); -// jdbcTemplate.execute("insert into bar values (1), (2), (3);"); -// -// memsqlNetworkTestOperations.withNetworkLatency(ofMillis(1000), -// () -> assertThat(durationOf(() -> jdbcTemplate.queryForList("select * from bar"))) -// .isGreaterThan(1000L) -// ); -// -// assertThat(durationOf(() -> jdbcTemplate.queryForList("select * from bar"))) -// .isLessThan(100L); -// } + @Test + public void shouldEmulateLatency() throws Exception { + jdbcTemplate.execute("create table bar (id int primary key);"); + jdbcTemplate.execute("insert into bar values (1), (2), (3);"); + + memsqlContainerProxy.toxics().latency("latency", ToxicDirection.UPSTREAM, 1000); + + assertThat(durationOf(() -> jdbcTemplate.queryForList("select * from bar"))) + .isCloseTo(1000L, Offset.offset(100L)); + + memsqlContainerProxy.toxics().get("latency").remove(); + + assertThat(durationOf(() -> jdbcTemplate.queryForList("select * from bar"))) + .isLessThan(100L); + } @Test public void shouldSetupDependsOnForAllDataSources() throws Exception { diff --git a/embedded-memsql/src/test/resources/application-enabled.properties b/embedded-memsql/src/test/resources/application-enabled.properties index 0982b4e6d..332e26a82 100644 --- a/embedded-memsql/src/test/resources/application-enabled.properties +++ b/embedded-memsql/src/test/resources/application-enabled.properties @@ -1,7 +1,7 @@ spring.datasource.driver-class-name=org.mariadb.jdbc.Driver -spring.datasource.url=jdbc:mariadb://${embedded.memsql.host}:${embedded.memsql.port}/${embedded.memsql.schema} +spring.datasource.url=jdbc:mariadb://${embedded.memsql.toxiproxy.host}:${embedded.memsql.toxiproxy.port}/${embedded.memsql.schema} spring.datasource.username=${embedded.memsql.user} spring.datasource.password=${embedded.memsql.password} spring.datasource.tomcat.test-on-borrow=true spring.datasource.tomcat.validation-query=SELECT 1; -spring.datasource.tomcat.test-on-connect=true \ No newline at end of file +spring.datasource.tomcat.test-on-connect=true diff --git a/embedded-memsql/src/test/resources/bootstrap-enabled.properties b/embedded-memsql/src/test/resources/bootstrap-enabled.properties index 68e08defc..99df0876e 100644 --- a/embedded-memsql/src/test/resources/bootstrap-enabled.properties +++ b/embedded-memsql/src/test/resources/bootstrap-enabled.properties @@ -1 +1,2 @@ -embedded.memsql.licenseKey=BGE5YzE5NTdmN2I1NDQ4MjhhNTQ0MTM4YWQ4Y2Q5ZWNiAAAAAAAAAAAEAAAAAAAAAAwwNQIYKbH2LOLeT189H+nBdRagLdLboVuJTsgJAhkAtSepSZkq0Zn4FDVtvZyASWzovR2OeeyiAA== \ No newline at end of file +embedded.memsql.licenseKey=BGE5YzE5NTdmN2I1NDQ4MjhhNTQ0MTM4YWQ4Y2Q5ZWNiAAAAAAAAAAAEAAAAAAAAAAwwNQIYKbH2LOLeT189H+nBdRagLdLboVuJTsgJAhkAtSepSZkq0Zn4FDVtvZyASWzovR2OeeyiAA== +embedded.toxiproxy.proxies.memsql.enabled=true diff --git a/embedded-minio/src/main/java/com/playtika/testcontainer/minio/DefaultMinioWaitStrategy.java b/embedded-minio/src/main/java/com/playtika/testcontainer/minio/DefaultMinioWaitStrategy.java deleted file mode 100644 index ecc175fdf..000000000 --- a/embedded-minio/src/main/java/com/playtika/testcontainer/minio/DefaultMinioWaitStrategy.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.playtika.testcontainer.minio; - -import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; -import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; -import org.testcontainers.containers.wait.strategy.WaitAllStrategy; - -public class DefaultMinioWaitStrategy extends WaitAllStrategy implements MinioWaitStrategy { - - public DefaultMinioWaitStrategy(MinioProperties properties) { - withStrategy(new HostPortWaitStrategy()) - .withStrategy(new HttpWaitStrategy() - .forPath("/minio/health/live") - .forPort(properties.port) - .forStatusCode(200)) - .withStartupTimeout(properties.getTimeoutDuration()); - } -} diff --git a/embedded-minio/src/main/java/com/playtika/testcontainer/minio/EmbeddedMinioBootstrapConfiguration.java b/embedded-minio/src/main/java/com/playtika/testcontainer/minio/EmbeddedMinioBootstrapConfiguration.java index dbb0bf5bf..16896fb53 100644 --- a/embedded-minio/src/main/java/com/playtika/testcontainer/minio/EmbeddedMinioBootstrapConfiguration.java +++ b/embedded-minio/src/main/java/com/playtika/testcontainer/minio/EmbeddedMinioBootstrapConfiguration.java @@ -10,15 +10,15 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.MinIOContainer; import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.toxiproxy.ToxiproxyContainer; import java.util.LinkedHashMap; @@ -31,28 +31,17 @@ @Configuration @ConditionalOnExpression("${embedded.containers.enabled:true}") @AutoConfigureAfter(DockerPresenceBootstrapConfiguration.class) +@EnableConfigurationProperties(MinioProperties.class) @ConditionalOnProperty(value = "embedded.minio.enabled", matchIfMissing = true) public class EmbeddedMinioBootstrapConfiguration { private static final String MINIO_NETWORK_ALIAS = "minio.testcontainer.docker"; - @Bean - @ConditionalOnMissingBean - MinioProperties minioProperties() { - return new MinioProperties(); - } - - @Bean(name = "minioWaitStrategy") - @ConditionalOnMissingBean - public MinioWaitStrategy minioWaitStrategy(MinioProperties properties) { - return new DefaultMinioWaitStrategy(properties); - } - @Bean @ConditionalOnToxiProxyEnabled(module = "minio") ToxiproxyClientProxy minioContainerProxy(ToxiproxyClient toxiproxyClient, ToxiproxyContainer toxiproxyContainer, - @Qualifier(BEAN_NAME_EMBEDDED_MINIO) GenericContainer minio, + @Qualifier(BEAN_NAME_EMBEDDED_MINIO) MinIOContainer minio, ConfigurableEnvironment environment, MinioProperties properties) { ToxiproxyClientProxy proxy = ToxiproxyHelper.createProxy( @@ -68,21 +57,19 @@ ToxiproxyClientProxy minioContainerProxy(ToxiproxyClient toxiproxyClient, } @Bean(name = BEAN_NAME_EMBEDDED_MINIO, destroyMethod = "stop") - public MinIOContainer minio(MinioWaitStrategy minioWaitStrategy, - ConfigurableEnvironment environment, - MinioProperties properties, - Optional network) { - + public MinIOContainer minio(ConfigurableEnvironment environment, + MinioProperties properties, + Optional network) { MinIOContainer minio = new MinIOContainer(ContainerUtils.getDockerImageName(properties)) - .withExposedPorts(properties.getPort(), properties.getConsolePort()) - .withEnv("MINIO_ROOT_USER", properties.getAccessKey()) - .withEnv("MINIO_ROOT_PASSWORD", properties.getSecretKey()) + .withUserName(properties.getAccessKey()) + .withPassword(properties.getSecretKey()) .withEnv("MINIO_SITE_REGION", properties.getRegion()) .withEnv("MINIO_WORM", properties.getWorm()) .withEnv("MINIO_BROWSER", properties.getBrowser()) - .withCommand("server", properties.getDirectory(), "--console-address", ":" + properties.getConsolePort()) - .waitingFor(minioWaitStrategy) + .waitingFor(Wait.forHttp("/minio/health/live") + .forPort(properties.getPort()) + .withStartupTimeout(properties.getTimeoutDuration())) .withNetworkAliases(MINIO_NETWORK_ALIAS); network.ifPresent(minio::withNetwork); @@ -110,5 +97,4 @@ private void registerEnvironment(MinIOContainer container, MapPropertySource propertySource = new MapPropertySource("embeddedMinioInfo", map); environment.getPropertySources().addFirst(propertySource); } - } diff --git a/embedded-minio/src/main/java/com/playtika/testcontainer/minio/MinioWaitStrategy.java b/embedded-minio/src/main/java/com/playtika/testcontainer/minio/MinioWaitStrategy.java deleted file mode 100644 index c92b5d1f7..000000000 --- a/embedded-minio/src/main/java/com/playtika/testcontainer/minio/MinioWaitStrategy.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.playtika.testcontainer.minio; - -import org.testcontainers.containers.wait.strategy.WaitStrategy; - -public interface MinioWaitStrategy extends WaitStrategy { -} diff --git a/embedded-minio/src/test/java/com/playtika/testcontainer/minio/EmbeddedMinioBootstrapConfigurationTest.java b/embedded-minio/src/test/java/com/playtika/testcontainer/minio/EmbeddedMinioBootstrapConfigurationTest.java index f5571ebc6..8e2026b5e 100644 --- a/embedded-minio/src/test/java/com/playtika/testcontainer/minio/EmbeddedMinioBootstrapConfigurationTest.java +++ b/embedded-minio/src/test/java/com/playtika/testcontainer/minio/EmbeddedMinioBootstrapConfigurationTest.java @@ -98,7 +98,7 @@ private String getFilePath(String name) { } private static String convertStreamToString(java.io.InputStream is) { - java.util.Scanner s = new java.util.Scanner(is, StandardCharsets.UTF_8.name()).useDelimiter("\\A"); + java.util.Scanner s = new java.util.Scanner(is, StandardCharsets.UTF_8).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } diff --git a/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapAuthConfigurationTest.java b/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapAuthConfigurationTest.java index 8a182d9e9..64fc755c3 100644 --- a/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapAuthConfigurationTest.java +++ b/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapAuthConfigurationTest.java @@ -1,7 +1,6 @@ package com.playtika.testcontainer.mongodb; -import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -57,13 +56,7 @@ public void propertiesAreAvailable() { assertThat(environment.getProperty("embedded.mongodb.database")).isNotEmpty(); } - @Value - static class Foo { - @Id - String someId; - String someString; - Instant someTimestamp; - Long someNumber; + record Foo(@Id String someId, String someString, Instant someTimestamp, Long someNumber) { } @EnableAutoConfiguration diff --git a/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapConfigurationTest.java b/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapConfigurationTest.java index 4cd9bc481..c19085c44 100644 --- a/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapConfigurationTest.java +++ b/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapConfigurationTest.java @@ -2,7 +2,6 @@ import com.playtika.testcontainer.toxiproxy.ToxiproxyClientProxy; import eu.rekawek.toxiproxy.model.ToxicDirection; -import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.assertj.core.data.Offset; import org.junit.jupiter.api.Test; @@ -74,13 +73,7 @@ private static long durationOf(Callable op) throws Exception { return System.currentTimeMillis() - start; } - @Value - static class Foo { - @Id - String someId; - String someString; - Instant someTimestamp; - Long someNumber; + record Foo(@Id String someId, String someString, Instant someTimestamp, Long someNumber) { } @EnableAutoConfiguration diff --git a/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapReplicaSetConfigurationTest.java b/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapReplicaSetConfigurationTest.java index 99f2c7b0c..e0ea2d97e 100644 --- a/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapReplicaSetConfigurationTest.java +++ b/embedded-mongodb/src/test/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapReplicaSetConfigurationTest.java @@ -1,7 +1,6 @@ package com.playtika.testcontainer.mongodb; -import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -55,13 +54,7 @@ public void propertiesAreAvailable() { assertThat(environment.getProperty("embedded.mongodb.replica-set-name")).isNotEmpty(); } - @Value - static class Foo { - @Id - String someId; - String someString; - Instant someTimestamp; - Long someNumber; + record Foo(@Id String someId, String someString, Instant someTimestamp, Long someNumber) { } @EnableAutoConfiguration diff --git a/embedded-native-kafka/src/main/java/com/playtika/testcontainer/nativekafka/configuration/NativeKafkaContainerConfiguration.java b/embedded-native-kafka/src/main/java/com/playtika/testcontainer/nativekafka/configuration/NativeKafkaContainerConfiguration.java index a96dee6ed..22ae491e0 100644 --- a/embedded-native-kafka/src/main/java/com/playtika/testcontainer/nativekafka/configuration/NativeKafkaContainerConfiguration.java +++ b/embedded-native-kafka/src/main/java/com/playtika/testcontainer/nativekafka/configuration/NativeKafkaContainerConfiguration.java @@ -10,7 +10,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; -import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.kafka.KafkaContainer; @@ -50,13 +49,12 @@ public Network nativeKafkaNetwork() { } @Bean(name = NATIVE_KAFKA_BEAN_NAME, destroyMethod = "stop") - public GenericContainer nativeKafka( + public KafkaContainer nativeKafka( NativeKafkaConfigurationProperties nativeKafkaProperties, ConfigurableEnvironment environment, Network network) { - DockerImageName nativeKafkaImageName = DockerImageName.parse(nativeKafkaProperties.getDefaultDockerImage()) - .asCompatibleSubstituteFor("confluentinc/cp-kafka"); + DockerImageName nativeKafkaImageName = DockerImageName.parse(nativeKafkaProperties.getDefaultDockerImage()); KafkaContainer nativeKafka = new KafkaContainer(nativeKafkaImageName) .withNetwork(network) @@ -92,16 +90,16 @@ private void configureFileSystemBind(NativeKafkaConfigurationProperties nativeKa log.info("Writing native kafka data to: {}", kafkaData); createPathAndParentOrMakeWritable(kafkaData); - nativeKafka.addFileSystemBind(kafkaData.toString(), "/tmp/kafka-logs", BindMode.READ_WRITE); + nativeKafka.withFileSystemBind(kafkaData.toString(), "/tmp/kafka-logs"); } } - private void registerNativeKafkaEnvironment(GenericContainer nativeKafka, + private void registerNativeKafkaEnvironment(KafkaContainer nativeKafka, ConfigurableEnvironment environment, NativeKafkaConfigurationProperties nativeKafkaProperties) { LinkedHashMap map = new LinkedHashMap<>(); - String bootstrapServers = ((KafkaContainer) nativeKafka).getBootstrapServers(); + String bootstrapServers = nativeKafka.getBootstrapServers(); map.put("embedded.kafka.bootstrapServers", bootstrapServers); map.put("embedded.kafka.brokerList", bootstrapServers); map.put("embedded.kafka.networkAlias", NATIVE_KAFKA_HOST_NAME); @@ -150,4 +148,4 @@ private void makeWritable(Path path) { throw new RuntimeException(e); } } -} \ No newline at end of file +} diff --git a/embedded-native-kafka/src/test/java/com/playtika/testcontainer/nativekafka/EmbeddedNativeKafkaWithBindingTest.java b/embedded-native-kafka/src/test/java/com/playtika/testcontainer/nativekafka/EmbeddedNativeKafkaWithBindingTest.java index 72c9a37eb..32c7900a3 100644 --- a/embedded-native-kafka/src/test/java/com/playtika/testcontainer/nativekafka/EmbeddedNativeKafkaWithBindingTest.java +++ b/embedded-native-kafka/src/test/java/com/playtika/testcontainer/nativekafka/EmbeddedNativeKafkaWithBindingTest.java @@ -40,9 +40,7 @@ public static void afterAll(@Autowired NativeKafkaConfigurationProperties native nativeKafka.execInContainer("rm", "-rf", "/tmp/kafka-logs"); // Delete mounted directories after test run - Runtime.getRuntime().addShutdownHook(new Thread(DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, () -> { - PathUtils.recursiveDeleteDir(nativeKafkaDataFolder); - })); + Runtime.getRuntime().addShutdownHook(new Thread(DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, () -> PathUtils.recursiveDeleteDir(nativeKafkaDataFolder))); } protected static Path projectDir() { @@ -53,4 +51,4 @@ protected static Path projectDir() { throw new RuntimeException(e); } } -} \ No newline at end of file +} diff --git a/embedded-native-kafka/src/test/java/com/playtika/testcontainer/nativekafka/NativeKafkaTopicsConfigurerTest.java b/embedded-native-kafka/src/test/java/com/playtika/testcontainer/nativekafka/NativeKafkaTopicsConfigurerTest.java index 04f740bcf..edfbe389c 100644 --- a/embedded-native-kafka/src/test/java/com/playtika/testcontainer/nativekafka/NativeKafkaTopicsConfigurerTest.java +++ b/embedded-native-kafka/src/test/java/com/playtika/testcontainer/nativekafka/NativeKafkaTopicsConfigurerTest.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Properties; import java.util.concurrent.ExecutionException; @@ -111,7 +112,7 @@ void shouldCreateTopicsWithCustomPartitions() { when(adminClient.createTopics(anyCollection())).thenReturn(createTopicsResult); when(createTopicsResult.all()).thenReturn(future); - Collection topicsToCreate = Arrays.asList("topic1"); + Collection topicsToCreate = List.of("topic1"); Collection topicsConfiguration = Arrays.asList( new TopicConfiguration("topic1", 5), new TopicConfiguration("topic2", 3) @@ -149,8 +150,8 @@ void shouldMergeTopicsWithCustomConfigurationTakingPrecedence() { when(createTopicsResult.all()).thenReturn(future); Collection topicsToCreate = Arrays.asList("topic1", "topic2"); - Collection topicsConfiguration = Arrays.asList( - new TopicConfiguration("topic1", 10) + Collection topicsConfiguration = List.of( + new TopicConfiguration("topic1", 10) ); configurer.createTopics(topicsToCreate, topicsConfiguration); @@ -196,7 +197,7 @@ void shouldHandleExecutionExceptionDuringTopicCreation() throws Exception { when(createTopicsResult.all()).thenReturn(future); when(future.get()).thenThrow(new ExecutionException(new TopicExistsException("Topic exists"))); - Collection topicsToCreate = Arrays.asList("topic1"); + Collection topicsToCreate = List.of("topic1"); Collection topicsConfiguration = Collections.emptyList(); assertThatThrownBy(() -> configurer.createTopics(topicsToCreate, topicsConfiguration)) @@ -217,7 +218,7 @@ void shouldHandleInterruptedExceptionDuringTopicCreation() throws Exception { when(createTopicsResult.all()).thenReturn(future); when(future.get()).thenThrow(new InterruptedException("Interrupted")); - Collection topicsToCreate = Arrays.asList("topic1"); + Collection topicsToCreate = List.of("topic1"); Collection topicsConfiguration = Collections.emptyList(); assertThatThrownBy(() -> configurer.createTopics(topicsToCreate, topicsConfiguration)) @@ -237,7 +238,7 @@ void shouldCreateTopicsWithReplicationFactorOne() { when(adminClient.createTopics(anyCollection())).thenReturn(createTopicsResult); when(createTopicsResult.all()).thenReturn(future); - Collection topicsToCreate = Arrays.asList("topic1"); + Collection topicsToCreate = List.of("topic1"); Collection topicsConfiguration = Collections.emptyList(); configurer.createTopics(topicsToCreate, topicsConfiguration); @@ -262,8 +263,8 @@ void shouldCreateOnlyConfiguredTopicsWhenNoTopicsToCreateSpecified() { when(createTopicsResult.all()).thenReturn(future); Collection topicsToCreate = Collections.emptyList(); - Collection topicsConfiguration = Arrays.asList( - new TopicConfiguration("configuredTopic", 2) + Collection topicsConfiguration = List.of( + new TopicConfiguration("configuredTopic", 2) ); configurer.createTopics(topicsToCreate, topicsConfiguration); @@ -277,4 +278,4 @@ void shouldCreateOnlyConfiguredTopicsWhenNoTopicsToCreateSpecified() { assertThat(topic.numPartitions()).isEqualTo(2); } } -} \ No newline at end of file +} diff --git a/embedded-nats/README.adoc b/embedded-nats/README.adoc index 2307a5a6d..56b683dfa 100644 --- a/embedded-nats/README.adoc +++ b/embedded-nats/README.adoc @@ -33,11 +33,17 @@ * `embedded.nats.internalClientPort` * `embedded.nats.internalHttpMonitorPort` * `embedded.nats.internalRouteConnectionsPort` +* Bean `NatsContainer embeddedNats` * Bean `ToxiproxyClientProxy natsContainerProxy` ==== Notes -Nats container has no security enabled, you can use any credentials. +Uses https://github.com/amadeusitgroup/nats-testcontainers[NatsContainer] from `io.github.amadeusitgroup.testcontainers:nats`. +Exposes ports 4222 (client), 6222 (routing), 8222 (HTTP monitoring). +No security enabled by default; any credentials are accepted. + +To enable JetStream or authentication, inject the `NatsContainer` bean and configure it before the context starts, +or use the `embedded.nats.command` property to pass flags (e.g. `--jetstream`). ===== Create client with ToxiProxy You can also create client pointed at ToxiProxy TCP proxy: diff --git a/embedded-nats/pom.xml b/embedded-nats/pom.xml index 4f32b24f4..0f6491bd8 100644 --- a/embedded-nats/pom.xml +++ b/embedded-nats/pom.xml @@ -14,6 +14,7 @@ embedded-nats + 1.0.9 2.25.3 @@ -22,6 +23,11 @@ com.playtika.testcontainers testcontainers-common + + io.github.amadeusitgroup.testcontainers + nats + ${nats-testcontainers.version} + org.testcontainers testcontainers-toxiproxy @@ -42,4 +48,4 @@ ${jnats.version} - \ No newline at end of file + diff --git a/embedded-nats/src/main/java/com/playtika/testcontainer/nats/EmbeddedNatsBootstrapConfiguration.java b/embedded-nats/src/main/java/com/playtika/testcontainer/nats/EmbeddedNatsBootstrapConfiguration.java index 06e98b43c..b08f8dabb 100644 --- a/embedded-nats/src/main/java/com/playtika/testcontainer/nats/EmbeddedNatsBootstrapConfiguration.java +++ b/embedded-nats/src/main/java/com/playtika/testcontainer/nats/EmbeddedNatsBootstrapConfiguration.java @@ -7,6 +7,7 @@ import com.playtika.testcontainer.toxiproxy.ToxiproxyHelper; import com.playtika.testcontainer.toxiproxy.condition.ConditionalOnToxiProxyEnabled; import eu.rekawek.toxiproxy.ToxiproxyClient; +import io.github.amadeusitgroup.testcontainers.nats.NatsContainer; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -17,13 +18,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; -import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; -import org.testcontainers.containers.wait.strategy.WaitAllStrategy; -import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.toxiproxy.ToxiproxyContainer; -import org.testcontainers.utility.MountableFile; import java.util.LinkedHashMap; import java.util.Optional; @@ -46,14 +42,13 @@ public class EmbeddedNatsBootstrapConfiguration { @ConditionalOnToxiProxyEnabled(module = "nats") ToxiproxyClientProxy natsContainerProxy(ToxiproxyClient toxiproxyClient, ToxiproxyContainer toxiproxyContainer, - @Qualifier(BEAN_NAME_EMBEDDED_NATS) GenericContainer natsContainer, - NatsProperties properties, + @Qualifier(BEAN_NAME_EMBEDDED_NATS) NatsContainer natsContainer, ConfigurableEnvironment environment) { ToxiproxyClientProxy proxy = ToxiproxyHelper.createProxy( toxiproxyClient, toxiproxyContainer, natsContainer, - properties.getClientPort(), + NatsContainer.DEFAULT_NATS_CLIENT_PORT, "nats"); ToxiproxyHelper.registerProxyEnvironment(proxy, "embedded.nats", "embeddedNatsToxiproxyInfo", environment); @@ -62,45 +57,32 @@ ToxiproxyClientProxy natsContainerProxy(ToxiproxyClient toxiproxyClient, } @Bean(name = BEAN_NAME_EMBEDDED_NATS, destroyMethod = "stop") - public GenericContainer natsContainer(ConfigurableEnvironment environment, - NatsProperties properties, - Optional network) { - WaitStrategy waitStrategy = new WaitAllStrategy() - .withStrategy(new HostPortWaitStrategy()) - .withStartupTimeout(properties.getTimeoutDuration()); - - GenericContainer natsContainer = new GenericContainer<>(ContainerUtils.getDockerImageName(properties)) - .withExposedPorts(properties.getClientPort(), properties.getHttpMonitorPort(), properties.getRouteConnectionsPort()) - .withCopyFileToContainer(MountableFile.forClasspathResource("nats-server.conf"), "/nats-server.conf") - .waitingFor(waitStrategy) + public NatsContainer natsContainer(ConfigurableEnvironment environment, + NatsProperties properties, + Optional network) { + NatsContainer natsContainer = new NatsContainer(ContainerUtils.getDockerImageName(properties)) .withNetworkAliases(NATS_NETWORK_ALIAS); network.ifPresent(natsContainer::withNetwork); - natsContainer = configureCommonsAndStart(natsContainer, properties, log); + natsContainer = (NatsContainer) configureCommonsAndStart(natsContainer, properties, log); - registerNatsEnvironment(natsContainer, environment, properties); + registerNatsEnvironment(natsContainer, environment); return natsContainer; } - private void registerNatsEnvironment(GenericContainer natsContainer, - ConfigurableEnvironment environment, - NatsProperties properties) { - Integer clientMappedPort = natsContainer.getMappedPort(properties.getClientPort()); - Integer httpMonitorMappedPort = natsContainer.getMappedPort(properties.getHttpMonitorPort()); - Integer routeConnectionsMappedPort = natsContainer.getMappedPort(properties.getRouteConnectionsPort()); - String host = natsContainer.getHost(); - + private void registerNatsEnvironment(NatsContainer natsContainer, + ConfigurableEnvironment environment) { LinkedHashMap map = new LinkedHashMap<>(); - map.put("embedded.nats.host", host); - map.put("embedded.nats.port", clientMappedPort); - map.put("embedded.nats.httpMonitorPort", httpMonitorMappedPort); - map.put("embedded.nats.routeConnectionsPort", routeConnectionsMappedPort); + map.put("embedded.nats.host", natsContainer.getHost()); + map.put("embedded.nats.port", natsContainer.getClientPort()); + map.put("embedded.nats.httpMonitorPort", natsContainer.getHttpMonitoringPort()); + map.put("embedded.nats.routeConnectionsPort", natsContainer.getRoutingPort()); map.put("embedded.nats.networkAlias", NATS_NETWORK_ALIAS); - map.put("embedded.nats.internalClientPort", properties.getClientPort()); - map.put("embedded.nats.internalHttpMonitorPort", properties.getHttpMonitorPort()); - map.put("embedded.nats.internalRouteConnectionsPort", properties.getRouteConnectionsPort()); + map.put("embedded.nats.internalClientPort", NatsContainer.DEFAULT_NATS_CLIENT_PORT); + map.put("embedded.nats.internalHttpMonitorPort", NatsContainer.DEFAULT_NATS_HTTP_MONITORING_PORT); + map.put("embedded.nats.internalRouteConnectionsPort", NatsContainer.DEFAULT_NATS_ROUTING_PORT); log.info("Started NATS server. Connection details {}", map); diff --git a/embedded-nats/src/test/java/com/playtika/testcontainer/nats/BaseNatsTest.java b/embedded-nats/src/test/java/com/playtika/testcontainer/nats/BaseNatsTest.java index 2f6fb2961..62859c0a3 100644 --- a/embedded-nats/src/test/java/com/playtika/testcontainer/nats/BaseNatsTest.java +++ b/embedded-nats/src/test/java/com/playtika/testcontainer/nats/BaseNatsTest.java @@ -34,7 +34,7 @@ static class TestConfiguration { public Connection natsConnection(@Value("${embedded.nats.host}") String host, @Value("${embedded.nats.port}") int port) throws IOException, InterruptedException { - Options connectionOptions = getConnectionOptions(host, port, 100); + Options connectionOptions = getConnectionOptions(host, port); return Nats.connect(connectionOptions); } @@ -42,12 +42,12 @@ public Connection natsConnection(@Value("${embedded.nats.host}") String host, @ConditionalOnToxiProxyEnabled public Connection natsToxicproxyConnection(@Value("${embedded.nats.toxiproxy.host}") String host, @Value("${embedded.nats.toxiproxy.port}") int port) throws IOException, InterruptedException { - Options connectionOptions = getConnectionOptions(host, port, 1000); + Options connectionOptions = getConnectionOptions(host, port); return Nats.connect(connectionOptions); } } - private static Options getConnectionOptions(String host, int port, int timeoutInMillis) { + private static Options getConnectionOptions(String host, int port) { return new Options.Builder() .server(String.format("nats://%s:%s", host, port)) .build(); diff --git a/embedded-neo4j/src/test/java/com/playtika/testcontainer/neo4j/Person.java b/embedded-neo4j/src/test/java/com/playtika/testcontainer/neo4j/Person.java index 135a5c5f9..4b33d8f8f 100644 --- a/embedded-neo4j/src/test/java/com/playtika/testcontainer/neo4j/Person.java +++ b/embedded-neo4j/src/test/java/com/playtika/testcontainer/neo4j/Person.java @@ -13,7 +13,6 @@ import java.util.Set; import static java.util.Collections.emptySet; -import static java.util.stream.Collectors.toList; @Node @NoArgsConstructor @@ -52,6 +51,6 @@ public String toString() { .orElse(emptySet()) .stream() .map(teamMateRelationship -> teamMateRelationship.getTeamMate().getName()) - .collect(toList()); + .toList(); } } diff --git a/embedded-redis/src/main/java/com/playtika/testcontainer/redis/wait/RedisClusterStatusCheck.java b/embedded-redis/src/main/java/com/playtika/testcontainer/redis/wait/RedisClusterStatusCheck.java index a1f0af652..ad39e68d5 100644 --- a/embedded-redis/src/main/java/com/playtika/testcontainer/redis/wait/RedisClusterStatusCheck.java +++ b/embedded-redis/src/main/java/com/playtika/testcontainer/redis/wait/RedisClusterStatusCheck.java @@ -39,11 +39,16 @@ private void logClusterInfo() { String info = jedis.info(); Map config = jedis.configGet("*"); String clusterNodes = jedis.clusterNodes(); - log.error("Cluster in failed state:\n" + - "-- cluster info:\n{}\n" + - "-- nodes:\n{}\n" + - "-- info:\n{}\n" + - "-- config:\n{}", + log.error(""" + Cluster in failed state: + -- cluster info: + {} + -- nodes: + {} + -- info: + {} + -- config: + {}""", clusterInfo, clusterNodes, info, String.join("\n", config.values())); } } diff --git a/embedded-selenium/README.adoc b/embedded-selenium/README.adoc index bb71940ed..fa5fcc9bf 100644 --- a/embedded-selenium/README.adoc +++ b/embedded-selenium/README.adoc @@ -15,10 +15,10 @@ ==== Consumes (via `bootstrap.properties`) * `embedded.selenium.enabled` `(true|false, default is true)` -* `embedded.selenium.browser` `(enum, FIREFOX|CHROMIUM, default is 'CHROMIUM')` -* `embedded.selenium.docker-image` `(Docker image like "selenium/standalone-chrome-debug:3.141.59")` -** Image versions on https://hub.docker.com/u/selenium[dockerhub] repositories -** Prefix "standalone-" has a browser installed, suffix "-debug" additionally a VNC server for https://github.com/SeleniumHQ/docker-selenium[docker-selenium] +* `embedded.selenium.docker-image` `(Docker image, e.g. "selenium/standalone-chrome:4.x.x")` +** Available images on https://hub.docker.com/u/selenium[Docker Hub] +** Use `selenium/standalone-chrome`, `selenium/standalone-firefox`, or `selenium/standalone-edge` +* `embedded.selenium.arguments` `(list of browser arguments passed to ChromeOptions/FirefoxOptions)` * `embedded.selenium.vnc.mode` `RECORD_ALL | SKIP | RECORD_FAILING` * `embedded.selenium.vnc.recording-dir` `a file location` * `embedded.toxiproxy.proxies.selenium.enabled` Enables both creation of the container with ToxiProxy TCP proxy and a proxy to the `embedded-selenium` container. @@ -29,7 +29,7 @@ * `embedded.selenium.port` * `embedded.selenium.host` * `embedded.selenium.vnc.host` -* `embedded.selenium.vnc.port` (mapped HTTP port) +* `embedded.selenium.vnc.port` * `embedded.selenium.vnc.username` * `embedded.selenium.vnc.password` * `embedded.selenium.toxiproxy.host` @@ -37,91 +37,92 @@ * `embedded.selenium.networkAlias` * Bean `ToxiproxyClientProxy seleniumContainerProxy` -==== Example +==== Browser selection -There is currently no starter library for using selenium server so there is no need to -use the produced variables. You can control the configuration of the browser in 2 ways. -The first is via the properties file: +The browser is determined by the Docker image, not by capabilities. +Use the appropriate standalone image for the browser you need: -./src/test/resources/application.yaml -[source,yaml] +[cols="1,2"] +|=== +|Browser |Image + +|Chrome +|`selenium/standalone-chrome:4.x.x` + +|Firefox +|`selenium/standalone-firefox:4.x.x` + +|Edge +|`selenium/standalone-edge:4.x.x` +|=== + +==== Usage + +The container exposes a standard Selenium WebDriver endpoint. Create a `RemoteWebDriver` using the container's address and pass browser options at session creation time: + +[source,java] ---- -embedded: - selenium: - enabled: true - browser: CHROMIUM - arguments: - - ignore-certificate-errors +@Autowired +private BrowserWebDriverContainer container; +@Test +void test() { + ChromeOptions options = new ChromeOptions(); + options.addArguments("--headless"); + + RemoteWebDriver driver = new RemoteWebDriver(container.getSeleniumAddress(), options); + driver.get("http://" + dockerHostname + ":" + port); + assertThat(driver.getTitle()).isEqualTo("Hello World"); +} ---- -The second (which may be more configurable) is via defining either a -ChromeOptions class or a FirefoxOptions class in your test class: +Browser arguments from `embedded.selenium.arguments` are pre-applied via the auto-configured `ChromeOptions` or `FirefoxOptions` bean. You can override or extend it with a `@TestConfiguration`: + [source,java] ------------------ - @TestConfiguration - static class LocalTestConfiguration { - - @Bean - public FirefoxOptions firefoxOptions(SeleniumProperties properties) { - - FirefoxOptions options = new FirefoxOptions(); - properties.apply(options); - options.addArguments("hello-world"); - options.setCapability(CapabilityType.ACCEPT_SSL_CERTS, false); - options.setCapability(CapabilityType.ACCEPT_INSECURE_CERTS, false); - return options; - } +---- +@TestConfiguration +static class TestConfig { + + @Bean + public ChromeOptions chromeOptions(SeleniumProperties properties) { + ChromeOptions options = new ChromeOptions(); + properties.apply(options); + options.addArguments("--disable-gpu"); + return options; } ------------------ -If you are testing your local machine you can access the hostname via the annotation +} +---- + +To get the Docker host address (e.g. to reach your Spring Boot app from inside the container): [source,java] ------------------ - @DockerHostname - private String hostname; ------------------ +---- +@DockerHostname +private String dockerHostname; +---- -An example test class would look like the following: +Full example: [source,java] ------------------ -import org.openqa.selenium.WebDriver;@RunWith(SpringRunner.class) -@SpringBootTest( - classes = SpringBootWebApplication.class, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT -) -@Import(TestLoginPage.LocalTestConfiguration.class) -public class TestLoginPage { +---- +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class MySeleniumTest { + + @Autowired + private BrowserWebDriverContainer container; @LocalServerPort private int port; @DockerHostname - private String hostname; - - @Autowired - private BrowserWebDriverContainer webDriverContainer; + private String dockerHostname; @Test - public void test1() { - RemoteWebDriver webDriver = webDriverContainer.getWebDriver(); - webDriver.get("https://" + hostname +":" + port); - assertThat(webDriver.getPageSource()).contains("helloworld"); - } - - @TestConfiguration - static class LocalTestConfiguration { - - @Bean - public FirefoxOptions firefoxOptions(SeleniumProperties properties) { - - FirefoxOptions options = new FirefoxOptions(); - properties.apply(options); - options.setCapability(CapabilityType.ACCEPT_SSL_CERTS, true); - options.setCapability(CapabilityType.ACCEPT_INSECURE_CERTS, true); - return options; - } + void pageLoads() { + RemoteWebDriver driver = new RemoteWebDriver( + container.getSeleniumAddress(), new ChromeOptions()); + driver.get("http://" + dockerHostname + ":" + port + "/index.html"); + assertThat(driver.getTitle()).isEqualTo("My App"); } } ------------------ +---- diff --git a/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/EmbeddedSeleniumBootstrapConfiguration.java b/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/EmbeddedSeleniumBootstrapConfiguration.java index 5033beeab..98de726af 100644 --- a/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/EmbeddedSeleniumBootstrapConfiguration.java +++ b/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/EmbeddedSeleniumBootstrapConfiguration.java @@ -16,31 +16,25 @@ import org.springframework.context.annotation.Bean; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; -import org.testcontainers.containers.BrowserWebDriverContainer; import org.testcontainers.containers.Container; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.DefaultRecordingFileFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.RecordingFileFactory; -import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; -import org.testcontainers.containers.wait.strategy.WaitAllStrategy; -import org.testcontainers.containers.wait.strategy.WaitStrategy; +import org.testcontainers.selenium.BrowserWebDriverContainer; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URL; import java.nio.file.Files; -import java.time.Duration; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.Optional; import static com.playtika.testcontainer.selenium.SeleniumProperties.BEAN_NAME_EMBEDDED_SELENIUM; -import static java.time.temporal.ChronoUnit.SECONDS; @Slf4j @@ -81,17 +75,11 @@ public FirefoxOptions firefoxOptions(SeleniumProperties properties) { @ConditionalOnMissingBean public BrowserWebDriverContainer selenium(ConfigurableEnvironment environment, SeleniumProperties properties, - MutableCapabilities capabilities, Optional network) { - BrowserWebDriverContainer container = isNotBlank(properties.getDockerImage()) - ? new BrowserWebDriverContainer<>(ContainerUtils.getDockerImageName(properties)) - : new BrowserWebDriverContainer<>(); - - container.waitingFor(getWaitStrategy()); - container.withCapabilities(capabilities); - container.withRecordingFileFactory(getRecordingFileFactory()); - container.withNetworkAliases(SELENIUM_NETWORK_ALIAS); + BrowserWebDriverContainer container = new BrowserWebDriverContainer(ContainerUtils.getDockerImageName(properties)) + .withRecordingFileFactory(getRecordingFileFactory()) + .withNetworkAliases(SELENIUM_NETWORK_ALIAS); network.ifPresent(container::withNetwork); File recordingDirOrNull = null; if (properties.getVnc().getMode().convert() != BrowserWebDriverContainer.VncRecordingMode.SKIP) { @@ -106,19 +94,6 @@ public BrowserWebDriverContainer selenium(ConfigurableEnvironment environment, return container; } - //See: https://github.com/testcontainers/testcontainers-java/pull/4357 - @Deprecated - private WaitStrategy getWaitStrategy() { - WaitStrategy logWaitStrategy = new LogMessageWaitStrategy() - .withRegEx(".*(RemoteWebDriver instances should connect to|Selenium Server is up and running).*\n") - .withStartupTimeout(Duration.of(60, SECONDS)); - - return new WaitAllStrategy() - .withStrategy(logWaitStrategy) - .withStrategy(new HostPortWaitStrategy()) - .withStartupTimeout(Duration.of(60, SECONDS)); - } - /** * Testcontainers does not expose its default vnc dir when it is not * defined, so we recreate this implementation here. @@ -187,11 +162,11 @@ public String getHostName(GenericContainer container) { // unfortunately host.docker.internal only works for mac and windows :( // and we need to work out the hostname for linux. String OS = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH); - if ((OS.indexOf("mac") >= 0) || (OS.indexOf("darwin") >= 0)) { + if ((OS.contains("mac")) || (OS.contains("darwin"))) { return "host.docker.internal"; - } else if (OS.indexOf("win") >= 0) { + } else if (OS.contains("win")) { return "host.docker.internal"; - } else if (OS.indexOf("nux") >= 0) { + } else if (OS.contains("nux")) { Container.ExecResult execResult; try { execResult = container.execInContainer("/sbin/ip route|awk '/default/ { print $3 }'"); @@ -210,9 +185,7 @@ public String getHostName(GenericContainer container) { } } - // currently only supported if docker machine is installed. - // otherwise throws an UnsupportedOpertaion exception - return container.getTestHostIpAddress(); + return DOCKER_FOR_LINUX_STATIC_IP; } private boolean isValidIpAddress(String ipAddress) { @@ -222,7 +195,4 @@ private boolean isValidIpAddress(String ipAddress) { return InetAddresses.isInetAddress(ipAddress); } - private boolean isNotBlank(String str) { - return str != null && !str.trim().isEmpty(); - } -} \ No newline at end of file +} diff --git a/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/VncRecordingMode.java b/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/VncRecordingMode.java index d28ab7be3..95ef367f8 100644 --- a/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/VncRecordingMode.java +++ b/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/VncRecordingMode.java @@ -1,10 +1,10 @@ package com.playtika.testcontainer.selenium; -import org.testcontainers.containers.BrowserWebDriverContainer; +import org.testcontainers.selenium.BrowserWebDriverContainer; /** - * See {@link org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode} + * See {@link org.testcontainers.selenium.BrowserWebDriverContainer.VncRecordingMode} */ public enum VncRecordingMode { SKIP, RECORD_ALL, RECORD_FAILING; @@ -12,4 +12,4 @@ public enum VncRecordingMode { public BrowserWebDriverContainer.VncRecordingMode convert() { return BrowserWebDriverContainer.VncRecordingMode.valueOf(this.name()); } -} \ No newline at end of file +} diff --git a/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerContextCustomizerFactory.java b/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerContextCustomizerFactory.java index 9f8a3565b..e17c87afd 100644 --- a/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerContextCustomizerFactory.java +++ b/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerContextCustomizerFactory.java @@ -27,10 +27,7 @@ public boolean equals(Object obj) { if (obj == this) { return true; } - if (obj == null || obj.getClass() != getClass()) { - return false; - } - return true; + return obj != null && obj.getClass() == getClass(); } @Override @@ -38,4 +35,4 @@ public int hashCode() { return getClass().hashCode(); } } -} \ No newline at end of file +} diff --git a/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerScope.java b/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerScope.java index 3e119c36d..e255ee40c 100644 --- a/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerScope.java +++ b/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerScope.java @@ -48,16 +48,6 @@ public Object remove(String name) { public void registerDestructionCallback(String name, Runnable callback) { } - @Override - public Object resolveContextualObject(String key) { - return null; - } - - @Override - public String getConversationId() { - return null; - } - /** * Register this scope with the specified context and reassign appropriate bean * definitions to used it. @@ -102,9 +92,7 @@ public void afterTestMethod(TestContext testContext) { instances.values().stream() .filter(TestLifecycleAware.class::isInstance) .map(TestLifecycleAware.class::cast) - .forEach(value -> { - value.afterTest(testDescription(testContext), Optional.ofNullable(testContext.getTestException())); - }); + .forEach(value -> value.afterTest(testDescription(testContext), Optional.ofNullable(testContext.getTestException()))); } private TestDescription testDescription(TestContext testContext) { @@ -126,8 +114,6 @@ public void beforeTestMethod(TestContext testContext) { instances.values().stream() .filter(TestLifecycleAware.class::isInstance) .map(TestLifecycleAware.class::cast) - .forEach(value -> { - value.beforeTest(testDescription(testContext)); - }); + .forEach(value -> value.beforeTest(testDescription(testContext))); } -} \ No newline at end of file +} diff --git a/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerTestExecutionListener.java b/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerTestExecutionListener.java index ab1e44edf..b76f6027d 100644 --- a/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerTestExecutionListener.java +++ b/embedded-selenium/src/main/java/com/playtika/testcontainer/selenium/testscope/TestcontainerTestExecutionListener.java @@ -22,9 +22,7 @@ public void beforeTestMethod(TestContext testContext) { public void afterTestMethod(TestContext testContext) { TestcontainerScope scope = TestcontainerScope.getFrom(testContext.getApplicationContext()); scope.afterTestMethod(testContext); - if (scope != null ) { - testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE, - Boolean.TRUE); - } + testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE, + Boolean.TRUE); } -} \ No newline at end of file +} diff --git a/embedded-selenium/src/test/java/com/playtika/testcontainer/selenium/drivers/BaseEmbeddedSeleniumTest.java b/embedded-selenium/src/test/java/com/playtika/testcontainer/selenium/drivers/BaseEmbeddedSeleniumTest.java index 3336ac87c..848155257 100644 --- a/embedded-selenium/src/test/java/com/playtika/testcontainer/selenium/drivers/BaseEmbeddedSeleniumTest.java +++ b/embedded-selenium/src/test/java/com/playtika/testcontainer/selenium/drivers/BaseEmbeddedSeleniumTest.java @@ -4,12 +4,13 @@ import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.openqa.selenium.By; +import org.openqa.selenium.MutableCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.core.env.ConfigurableEnvironment; -import org.testcontainers.containers.BrowserWebDriverContainer; +import org.testcontainers.selenium.BrowserWebDriverContainer; import static org.assertj.core.api.Assertions.assertThat; @@ -23,6 +24,9 @@ public abstract class BaseEmbeddedSeleniumTest { @Autowired protected BrowserWebDriverContainer container; + @Autowired + protected MutableCapabilities capabilities; + @Autowired protected ConfigurableEnvironment environment; @@ -32,16 +36,25 @@ public abstract class BaseEmbeddedSeleniumTest { @DockerHostname private String dockerHostname; + private RemoteWebDriver driver; + + protected RemoteWebDriver getDriver() { + if (driver == null) { + driver = new RemoteWebDriver(container.getSeleniumAddress(), capabilities); + } + return driver; + } + @Test public void seleniumShouldWork() { - RemoteWebDriver driver = container.getWebDriver(); + RemoteWebDriver driver = getDriver(); getIndexPage(driver); assertThat(driver.getTitle()).isEqualTo("Hello World Page"); } @Test public void seleniumLinkShouldWorkAndPropertiesAreAvailable() { - RemoteWebDriver driver = container.getWebDriver(); + RemoteWebDriver driver = getDriver(); getIndexPage(driver); driver.findElement(By.linkText("Test Link")).click(); assertThat(driver.getTitle()).isEqualTo("Test Link Page"); @@ -60,7 +73,7 @@ private void getIndexPage(RemoteWebDriver driver) { } public String getBrowserName() { - return (String)container.getWebDriver().getCapabilities().getCapability("browserName"); + return (String) getDriver().getCapabilities().getCapability("browserName"); } } diff --git a/embedded-selenium/src/test/java/com/playtika/testcontainer/selenium/drivers/EmbeddedFirefoxSeleniumBeanConfigurationTest.java b/embedded-selenium/src/test/java/com/playtika/testcontainer/selenium/drivers/EmbeddedFirefoxSeleniumBeanConfigurationTest.java index 71e4f4bd3..45a75899e 100644 --- a/embedded-selenium/src/test/java/com/playtika/testcontainer/selenium/drivers/EmbeddedFirefoxSeleniumBeanConfigurationTest.java +++ b/embedded-selenium/src/test/java/com/playtika/testcontainer/selenium/drivers/EmbeddedFirefoxSeleniumBeanConfigurationTest.java @@ -2,7 +2,6 @@ import com.playtika.testcontainer.selenium.SeleniumProperties; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.openqa.selenium.firefox.FirefoxOptions; import org.openqa.selenium.remote.CapabilityType; @@ -10,6 +9,7 @@ import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; import java.util.List; import java.util.Map; @@ -21,7 +21,10 @@ * * {@link FirefoxOptions} */ -@Disabled +@TestPropertySource(properties = { + "embedded.selenium.browser=FIREFOX", + "embedded.selenium.docker-image=selenium/standalone-firefox-debug:3.141.59-mercury" +}) @Import(EmbeddedFirefoxSeleniumBeanConfigurationTest.LocalTestConfiguration.class) public class EmbeddedFirefoxSeleniumBeanConfigurationTest extends BaseEmbeddedSeleniumTest { @@ -44,7 +47,7 @@ public void testThatTestArgumentIsSet() { @Test public void testThatTestCapabilityIsSet() { // default is true, false is set in localtestconfiguration - boolean acceptUnsecureCertificates = (boolean)container.getWebDriver().getCapabilities().getCapability(CapabilityType.ACCEPT_INSECURE_CERTS); + boolean acceptUnsecureCertificates = (boolean) getDriver().getCapabilities().getCapability(CapabilityType.ACCEPT_INSECURE_CERTS); assertThat(acceptUnsecureCertificates).isFalse(); } diff --git a/embedded-temporal/src/test/java/com/playtika/testcontainer/temporal/EmbeddedTemporalBootstrapConfigurationTest.java b/embedded-temporal/src/test/java/com/playtika/testcontainer/temporal/EmbeddedTemporalBootstrapConfigurationTest.java index 768e01c2c..060c33158 100644 --- a/embedded-temporal/src/test/java/com/playtika/testcontainer/temporal/EmbeddedTemporalBootstrapConfigurationTest.java +++ b/embedded-temporal/src/test/java/com/playtika/testcontainer/temporal/EmbeddedTemporalBootstrapConfigurationTest.java @@ -26,7 +26,7 @@ import static io.temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED; import static java.util.UUID.randomUUID; import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.hasItem; +import static org.assertj.core.api.InstanceOfAssertFactories.list; class EmbeddedTemporalBootstrapConfigurationTest { @@ -41,7 +41,7 @@ class Headless { void workflowExecutionStarts(@Autowired WorkflowClient client) { String workflowId = startWorkflow(client); - assertThat(client.streamHistory(workflowId.toString())) + assertThat(client.streamHistory(workflowId)) .extracting(HistoryEvent::getEventType) .contains(EVENT_TYPE_WORKFLOW_EXECUTION_STARTED); } @@ -72,7 +72,7 @@ void workflowExecutionStarts( .expectStatus().isOk() .expectBody() .jsonPath("$.history.events[*].eventType") - .value(hasItem(EVENT_TYPE_WORKFLOW_EXECUTION_STARTED.toString())); + .value(values -> assertThat(values).asInstanceOf(list(String.class)).contains(EVENT_TYPE_WORKFLOW_EXECUTION_STARTED.toString())); } } diff --git a/embedded-vault/src/main/java/com/playtika/testcontainer/vault/EmbeddedVaultBootstrapConfiguration.java b/embedded-vault/src/main/java/com/playtika/testcontainer/vault/EmbeddedVaultBootstrapConfiguration.java index a31aadac2..ef9dbe65a 100644 --- a/embedded-vault/src/main/java/com/playtika/testcontainer/vault/EmbeddedVaultBootstrapConfiguration.java +++ b/embedded-vault/src/main/java/com/playtika/testcontainer/vault/EmbeddedVaultBootstrapConfiguration.java @@ -21,7 +21,6 @@ import org.testcontainers.toxiproxy.ToxiproxyContainer; import org.testcontainers.vault.VaultContainer; -import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Optional; @@ -67,7 +66,6 @@ public VaultContainer vault(ConfigurableEnvironment environment, VaultContainer vault = new VaultContainer<>(ContainerUtils.getDockerImageName(properties)) .withVaultToken(properties.getToken()) - .withExposedPorts(properties.getPort()) .withNetworkAliases(VAULT_NETWORK_ALIAS); network.ifPresent(vault::withNetwork); @@ -77,7 +75,7 @@ public VaultContainer vault(ConfigurableEnvironment environment, .toArray(String[]::new); if (secrets.length > 0) { - vault.withSecretInVault(properties.getPath(), secrets[0], Arrays.copyOfRange(secrets, 1, secrets.length)); + vault.withInitCommand("kv put " + properties.getPath() + " " + String.join(" ", secrets)); } if (properties.isCasEnabled()) { diff --git a/embedded-wiremock/README.adoc b/embedded-wiremock/README.adoc index e2f2d3042..9290b165d 100644 --- a/embedded-wiremock/README.adoc +++ b/embedded-wiremock/README.adoc @@ -18,7 +18,6 @@ * `embedded.wiremock.reuseContainer` `(true|false, default is false)` * `embedded.wiremock.dockerImage` `(default is 'wiremock/wiremock:3.13.2')` * `embedded.wiremock.host` `(default is 'localhost')` -* `embedded.wiremock.port` `(int, default is 8990)` ==== Produces @@ -27,7 +26,7 @@ * `embedded.wiremock.port` (mapped HTTP port) * `embedded.wiremock.networkAlias` * `embedded.wiremock.internalPort` -* Bean `GenericContainer embeddedWiremock` +* Bean `WireMockContainer embeddedWiremock` ==== Example diff --git a/embedded-wiremock/pom.xml b/embedded-wiremock/pom.xml index b50cb3777..8beee0baf 100644 --- a/embedded-wiremock/pom.xml +++ b/embedded-wiremock/pom.xml @@ -15,6 +15,7 @@ 3.13.2 + 1.0-alpha-13 @@ -22,6 +23,11 @@ com.playtika.testcontainers testcontainers-common + + org.wiremock.integrations.testcontainers + wiremock-testcontainers-module + ${wiremock-testcontainers-module.version} + org.wiremock @@ -36,4 +42,4 @@ test - \ No newline at end of file + diff --git a/embedded-wiremock/src/main/java/com/playtika/testcontainers/wiremock/EmbeddedWiremockBootstrapConfiguration.java b/embedded-wiremock/src/main/java/com/playtika/testcontainers/wiremock/EmbeddedWiremockBootstrapConfiguration.java index 3ce43c1a8..f6e9c1f0f 100644 --- a/embedded-wiremock/src/main/java/com/playtika/testcontainers/wiremock/EmbeddedWiremockBootstrapConfiguration.java +++ b/embedded-wiremock/src/main/java/com/playtika/testcontainers/wiremock/EmbeddedWiremockBootstrapConfiguration.java @@ -2,7 +2,8 @@ import com.playtika.testcontainer.common.spring.DockerPresenceBootstrapConfiguration; import com.playtika.testcontainer.common.utils.ContainerUtils; -import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -11,17 +12,14 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.containers.wait.strategy.WaitStrategy; +import org.wiremock.integrations.testcontainers.WireMockContainer; import java.util.LinkedHashMap; import java.util.Optional; import static com.playtika.testcontainer.common.utils.ContainerUtils.configureCommonsAndStart; -@Slf4j @Configuration @ConditionalOnExpression("${embedded.containers.enabled:true}") @AutoConfigureAfter(DockerPresenceBootstrapConfiguration.class) @@ -29,39 +27,32 @@ @EnableConfigurationProperties(WiremockProperties.class) public class EmbeddedWiremockBootstrapConfiguration { + private static final Logger log = LoggerFactory.getLogger(EmbeddedWiremockBootstrapConfiguration.class); + static final String BEAN_NAME_EMBEDDED_WIREMOCK = "embeddedWiremock"; private static final String WIREMOCK_NETWORK_ALIAS = "wiremock.testcontainer.docker"; - private static final WaitStrategy DEFAULT_WAITER = Wait.forHttp("/__admin/mappings") - .withMethod("GET") - .forStatusCode(200); @Bean(value = BEAN_NAME_EMBEDDED_WIREMOCK, destroyMethod = "stop") - public GenericContainer wiremockContainer(ConfigurableEnvironment environment, - WiremockProperties properties, - Optional network) { - GenericContainer wiremock = - new GenericContainer<>(ContainerUtils.getDockerImageName(properties)) - .waitingFor(DEFAULT_WAITER) - .withCommand("--port " + properties.getPort()) - .withExposedPorts(properties.getPort()) + public WireMockContainer wiremockContainer(ConfigurableEnvironment environment, + WiremockProperties properties, + Optional network) { + WireMockContainer wiremock = + new WireMockContainer(ContainerUtils.getDockerImageName(properties)) .withNetworkAliases(WIREMOCK_NETWORK_ALIAS); network.ifPresent(wiremock::withNetwork); - wiremock = configureCommonsAndStart(wiremock, properties, log); - registerWiremockEnvironment(wiremock, environment, properties); + wiremock = (WireMockContainer) configureCommonsAndStart(wiremock, properties, log); + registerWiremockEnvironment(wiremock, environment); return wiremock; } - private void registerWiremockEnvironment(GenericContainer container, ConfigurableEnvironment environment, WiremockProperties properties) { - Integer mappedPort = container.getMappedPort(properties.getPort()); - String host = container.getHost(); - + private void registerWiremockEnvironment(WireMockContainer container, ConfigurableEnvironment environment) { LinkedHashMap map = new LinkedHashMap<>(); - map.put("embedded.wiremock.port", mappedPort); - map.put("embedded.wiremock.host", host); + map.put("embedded.wiremock.port", container.getPort()); + map.put("embedded.wiremock.host", container.getHost()); map.put("embedded.wiremock.networkAlias", WIREMOCK_NETWORK_ALIAS); - map.put("embedded.wiremock.internalPort", properties.getPort()); + map.put("embedded.wiremock.internalPort", 8080); log.info("Started wiremock. Connection Details: {}", map); diff --git a/embedded-wiremock/src/main/java/com/playtika/testcontainers/wiremock/WiremockProperties.java b/embedded-wiremock/src/main/java/com/playtika/testcontainers/wiremock/WiremockProperties.java index 9095f86bd..5420f4527 100644 --- a/embedded-wiremock/src/main/java/com/playtika/testcontainers/wiremock/WiremockProperties.java +++ b/embedded-wiremock/src/main/java/com/playtika/testcontainers/wiremock/WiremockProperties.java @@ -11,7 +11,6 @@ public class WiremockProperties extends CommonContainerProperties { private String host = "localhost"; - private int port = 8990; // https://hub.docker.com/r/wiremock/wiremock @Override diff --git a/testcontainers-common/src/main/java/com/playtika/testcontainer/common/spring/AbstractDependsOnPostProcessor.java b/testcontainers-common/src/main/java/com/playtika/testcontainer/common/spring/AbstractDependsOnPostProcessor.java index a75e9a06d..abf3ec442 100644 --- a/testcontainers-common/src/main/java/com/playtika/testcontainer/common/spring/AbstractDependsOnPostProcessor.java +++ b/testcontainers-common/src/main/java/com/playtika/testcontainer/common/spring/AbstractDependsOnPostProcessor.java @@ -24,9 +24,7 @@ protected AbstractDependsOnPostProcessor(Class beansOfType, String ... depend public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { List beanNamesForType = asList(getBeanNamesForType(beanFactory)); beanNamesForType.forEach( - datastoreClientBeanName -> { - setupDependsOn(beanFactory, datastoreClientBeanName); - } + datastoreClientBeanName -> setupDependsOn(beanFactory, datastoreClientBeanName) ); } @@ -49,4 +47,4 @@ private String[] getBeanNamesForType(ConfigurableListableBeanFactory beanFactory private static List asList(String[] array) { return (array == null ? new ArrayList<>() : new ArrayList<>(Arrays.asList(array))); } -} \ No newline at end of file +} diff --git a/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/ContainerUtils.java b/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/ContainerUtils.java index 85e7f19ff..6910a3c82 100644 --- a/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/ContainerUtils.java +++ b/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/ContainerUtils.java @@ -84,7 +84,7 @@ public static GenericContainer configureCommonsAndStart(GenericContainer c } for (MountVolume mountVolume : properties.getMountVolumes()) { - updatedContainer.addFileSystemBind(mountVolume.getHostPath(), mountVolume.getContainerPath(), mountVolume.getMode()); + updatedContainer.withCopyToContainer(MountableFile.forHostPath(mountVolume.getHostPath()), mountVolume.getContainerPath()); } for (Capability capability : properties.getCapabilities()) { diff --git a/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/DateUtils.java b/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/DateUtils.java index c482291a1..0b802c3b2 100644 --- a/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/DateUtils.java +++ b/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/DateUtils.java @@ -23,11 +23,10 @@ public class DateUtils { */ public static String toDateAndTimeAgo(String isoFormattedDate) { Object instantOrString = parseToInstantOrString(isoFormattedDate); - if (!(instantOrString instanceof Instant)) { + if (!(instantOrString instanceof Instant instant)) { return (String) instantOrString; } - Instant instant = ((Instant) instantOrString); - OffsetDateTime offsetDateTime = ((Instant) instantOrString).atZone(ZoneId.systemDefault()).toOffsetDateTime(); + OffsetDateTime offsetDateTime = instant.atZone(ZoneId.systemDefault()).toOffsetDateTime(); return offsetDateTime + " (" + toTimeAgo(instant.toEpochMilli()) + ")"; } diff --git a/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/FileUtils.java b/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/FileUtils.java index dfbfc5b76..a4e050071 100644 --- a/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/FileUtils.java +++ b/testcontainers-common/src/main/java/com/playtika/testcontainer/common/utils/FileUtils.java @@ -25,7 +25,7 @@ public static Path resolveTemplateAsPath(ResourceLoader resourceLoader, String f String modifiedFile = resolveTemplateAsString(resourceLoader, fileName, modifyFunc); Path tempFilePath = Files.createTempFile("tc_", "_" + fileName); tempFilePath.toFile().deleteOnExit(); - Files.write(tempFilePath, modifiedFile.getBytes(StandardCharsets.UTF_8)); + Files.writeString(tempFilePath, modifiedFile); return tempFilePath; } diff --git a/testcontainers-common/src/test/java/com/playtika/testcontainer/common/utils/ContainerUtilsTest.java b/testcontainers-common/src/test/java/com/playtika/testcontainer/common/utils/ContainerUtilsTest.java index 02dc0fdb6..4fd3bdbd4 100644 --- a/testcontainers-common/src/test/java/com/playtika/testcontainer/common/utils/ContainerUtilsTest.java +++ b/testcontainers-common/src/test/java/com/playtika/testcontainer/common/utils/ContainerUtilsTest.java @@ -1,7 +1,5 @@ package com.playtika.testcontainer.common.utils; -import com.github.dockerjava.api.model.AccessMode; -import com.github.dockerjava.api.model.Bind; import com.playtika.testcontainer.bootstrap.EchoContainer; import com.playtika.testcontainer.common.checks.PositiveCommandWaitStrategy; import com.playtika.testcontainer.common.properties.CommonContainerProperties; @@ -13,8 +11,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.MountableFile; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -39,7 +40,7 @@ void tearDown() { } @Test - void configureCommonsAndStart() { + void configureCommonsAndStart() throws Exception { String[] command = {"/bin/sh", "-c", "while true; do echo 'Press [CTRL+C] to stop..'; sleep 1; done"}; Map env = new HashMap<>(); env.put("TEST_ENV_VAR", "VALUE_TEST"); @@ -71,21 +72,31 @@ public String getDefaultDockerImage() { assertThat(echoContainer.isShouldBeReused()).isTrue(); assertThat(echoContainer.getEnvMap()).containsAllEntriesOf(env); assertThat(echoContainer.getLogConsumers()).hasSize(1); - Condition> hasCopyToFileContainerPath = new Condition>() { + Condition> hasCopyToFileContainerPath = new Condition<>() { public boolean matches(Map.Entry mountableFileObjectEntry) { return mountableFileObjectEntry.getKey().getResolvedPath().endsWith(classpathResource) - && mountableFileObjectEntry.getValue().equals(containerPath); + && mountableFileObjectEntry.getValue().equals(containerPath); } }; assertThat(echoContainer.getCopyToFileContainerPathMap()).hasEntrySatisfying(hasCopyToFileContainerPath); + Map transferableMap = getCopyToTransferableContainerPathMap(echoContainer); + assertThat(transferableMap).hasSize(2); for (MountVolume mountVolume : mountVolumes) { - Condition hasMountBindings = new Condition<>( - bind -> MountableFile.forHostPath(mountVolume.getHostPath()).getResolvedPath().equals(bind.getPath()) - && mountVolume.getContainerPath().equals(bind.getVolume().getPath()) - && (BindMode.READ_WRITE.equals(mountVolume.getMode()) && AccessMode.rw == bind.getAccessMode() - || BindMode.READ_ONLY.equals(mountVolume.getMode()) && AccessMode.ro == bind.getAccessMode()), "binding"); - assertThat(echoContainer.getBinds()).hasSize(2).haveExactly(1, hasMountBindings); + String expectedPath = MountableFile.forHostPath(mountVolume.getHostPath()).getResolvedPath(); + Condition> hasMountVolume = new Condition<>( + entry -> entry.getKey() instanceof MountableFile mountableFile + && mountableFile.getResolvedPath().equals(expectedPath) + && mountVolume.getContainerPath().equals(entry.getValue()), + "mount volume copy"); + assertThat(transferableMap).hasEntrySatisfying(hasMountVolume); } } + + @SuppressWarnings("unchecked") + private static Map getCopyToTransferableContainerPathMap(EchoContainer container) throws Exception { + Field field = GenericContainer.class.getDeclaredField("copyToTransferableContainerPathMap"); + field.setAccessible(true); + return (Map) field.get(container); + } } diff --git a/testcontainers-common/src/test/java/com/playtika/testcontainer/common/utils/MountVolumesTest.java b/testcontainers-common/src/test/java/com/playtika/testcontainer/common/utils/MountVolumesTest.java index 91cbdd3ec..afb71823e 100644 --- a/testcontainers-common/src/test/java/com/playtika/testcontainer/common/utils/MountVolumesTest.java +++ b/testcontainers-common/src/test/java/com/playtika/testcontainer/common/utils/MountVolumesTest.java @@ -19,7 +19,7 @@ @ExtendWith(SpringExtension.class) public class MountVolumesTest { - ApplicationContextRunner context = new ApplicationContextRunner() + final ApplicationContextRunner context = new ApplicationContextRunner() .withUserConfiguration(PostgreSQLContainerPropertiesConfiguration.class); private static void assertMountVolume(CommonContainerProperties properties, String folder, String s, BindMode readOnly) {