diff --git a/modules/elasticsearch/build.gradle b/modules/elasticsearch/build.gradle index 56399a509d9..550e9a9b23a 100644 --- a/modules/elasticsearch/build.gradle +++ b/modules/elasticsearch/build.gradle @@ -6,3 +6,9 @@ dependencies { testImplementation "org.elasticsearch.client:transport:7.17.17" testImplementation 'org.assertj:assertj-core:3.25.2' } + +tasks.japicmp { + methodExcludes = [ + "org.testcontainers.elasticsearch.ElasticsearchContainer#containerIsStarted(com.github.dockerjava.api.command.InspectContainerResponse)", + ] +} diff --git a/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java index 940d6924c5e..e96ae26c02f 100644 --- a/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java +++ b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java @@ -1,6 +1,5 @@ package org.testcontainers.elasticsearch; -import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.exception.NotFoundException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; @@ -66,6 +65,9 @@ public class ElasticsearchContainer extends GenericContainer= 8 + private static final String DEFAULT_CERT_PATH = "/usr/share/elasticsearch/config/certs/http_ca.crt"; + /** * Elasticsearch Default version */ @@ -77,9 +79,7 @@ public class ElasticsearchContainer extends GenericContainer caCertAsBytes = Optional.empty(); - - private String certPath = "/usr/share/elasticsearch/config/certs/http_ca.crt"; + private String certPath = ""; /** * @deprecated use {@link #ElasticsearchContainer(DockerImageName)} instead @@ -91,6 +91,7 @@ public ElasticsearchContainer() { /** * Create an Elasticsearch Container by passing the full docker image name + * * @param dockerImageName Full docker image name as a {@link String}, like: docker.elastic.co/elasticsearch/elasticsearch:7.9.2 */ public ElasticsearchContainer(String dockerImageName) { @@ -99,6 +100,7 @@ public ElasticsearchContainer(String dockerImageName) { /** * Create an Elasticsearch Container by passing the full docker image name + * * @param dockerImageName Full docker image name as a {@link DockerImageName}, like: DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:7.9.2") */ public ElasticsearchContainer(final DockerImageName dockerImageName) { @@ -136,23 +138,7 @@ public ElasticsearchContainer(final DockerImageName dockerImageName) { setWaitStrategy(new LogMessageWaitStrategy().withRegEx(regex)); if (isAtLeastMajorVersion8) { withPassword(ELASTICSEARCH_DEFAULT_PASSWORD); - } - } - - @Override - protected void containerIsStarted(InspectContainerResponse containerInfo) { - if (isAtLeastMajorVersion8 && StringUtils.isNotEmpty(certPath)) { - try { - byte[] bytes = copyFileFromContainer(certPath, IOUtils::toByteArray); - if (bytes.length > 0) { - this.caCertAsBytes = Optional.of(bytes); - } - } catch (NotFoundException e) { - // just emit an error message, but do not throw an exception - // this might be ok, if the docker image is accidentally looking like version 8 or latest - // can happen if Elasticsearch is repackaged, i.e. with custom plugins - log.warn("CA cert under " + certPath + " not found."); - } + withCertPath(DEFAULT_CERT_PATH); } } @@ -162,17 +148,36 @@ protected void containerIsStarted(InspectContainerResponse containerInfo) { * @return byte array optional containing the CA cert extracted from the docker container */ public Optional caCertAsBytes() { - return caCertAsBytes; + if (StringUtils.isBlank(certPath)) { + return Optional.empty(); + } + try { + byte[] bytes = copyFileFromContainer(certPath, IOUtils::toByteArray); + if (bytes.length > 0) { + return Optional.of(bytes); + } + } catch (NotFoundException e) { + // just emit an error message, but do not throw an exception + // this might be ok, if the docker image is accidentally looking like version 8 or latest + // can happen if Elasticsearch is repackaged, i.e. with custom plugins + log.warn("CA cert under " + certPath + " not found."); + } + return Optional.empty(); } /** - * A SSL context based on the self signed CA, so that using this SSL Context allows to connect to the Elasticsearch service + * A SSL context based on the self-signed CA, so that using this SSL Context allows to connect to the Elasticsearch service * @return a customized SSL Context */ public SSLContext createSslContextFromCa() { try { CertificateFactory factory = CertificateFactory.getInstance("X.509"); - Certificate trustedCa = factory.generateCertificate(new ByteArrayInputStream(caCertAsBytes.get())); + Certificate trustedCa = factory.generateCertificate( + new ByteArrayInputStream( + caCertAsBytes() + .orElseThrow(() -> new IllegalStateException("CA cert under " + certPath + " not found.")) + ) + ); KeyStore trustStore = KeyStore.getInstance("pkcs12"); trustStore.load(null, null); trustStore.setCertificateEntry("ca", trustedCa); @@ -190,13 +195,13 @@ public SSLContext createSslContextFromCa() { /** * Define the Elasticsearch password to set. It enables security behind the scene for major version below 8.0.0. * It's not possible to use security with the oss image. - * @param password Password to set + * @param password Password to set * @return this */ public ElasticsearchContainer withPassword(String password) { if (isOss) { throw new IllegalArgumentException( - "You can not activate security on Elastic OSS Image. " + "Please switch to the default distribution" + "You can not activate security on Elastic OSS Image. Please switch to the default distribution" ); } withEnv("ELASTIC_PASSWORD", password); @@ -222,7 +227,8 @@ public String getHttpHostAddress() { return getHost() + ":" + getMappedPort(ELASTICSEARCH_DEFAULT_PORT); } - @Deprecated // The TransportClient will be removed in Elasticsearch 8. No need to expose this port anymore in the future. + // The TransportClient will be removed in Elasticsearch 8. No need to expose this port anymore in the future. + @Deprecated public InetSocketAddress getTcpHost() { return new InetSocketAddress(getHost(), getMappedPort(ELASTICSEARCH_DEFAULT_TCP_PORT)); } diff --git a/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java b/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java index 2b832f28a16..555cec58083 100644 --- a/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java +++ b/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java @@ -23,6 +23,7 @@ import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.RemoteDockerImage; +import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; @@ -375,6 +376,49 @@ public void testElasticsearch8SecureByDefaultFailsSilentlyOnLatestImages() throw } } + @Test + public void testElasticsearch7CanHaveSecurityEnabledAndUseSslContext() throws Exception { + String customizedCertPath = "/usr/share/elasticsearch/config/certs/http_ca_customized.crt"; + try ( + ElasticsearchContainer container = new ElasticsearchContainer( + "docker.elastic.co/elasticsearch/elasticsearch:7.17.15" + ) + .withPassword(ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD) + .withEnv("xpack.security.enabled", "true") + .withEnv("xpack.security.http.ssl.enabled", "true") + .withEnv("xpack.security.http.ssl.key", "/usr/share/elasticsearch/config/certs/elasticsearch.key") + .withEnv( + "xpack.security.http.ssl.certificate", + "/usr/share/elasticsearch/config/certs/elasticsearch.crt" + ) + .withEnv("xpack.security.http.ssl.certificate_authorities", customizedCertPath) + // these lines show how certificates can be created self-made way + // obviously this shouldn't be done in prod environment, where proper and officially signed keys should be present + .withCopyToContainer( + Transferable.of( + "#!/bin/bash\n" + + "mkdir -p /usr/share/elasticsearch/config/certs;" + + "openssl req -x509 -newkey rsa:4096 -keyout /usr/share/elasticsearch/config/certs/elasticsearch.key -out /usr/share/elasticsearch/config/certs/elasticsearch.crt -days 365 -nodes -subj \"/CN=localhost\";" + + "openssl x509 -outform der -in /usr/share/elasticsearch/config/certs/elasticsearch.crt -out " + + customizedCertPath + + "; chown -R elasticsearch /usr/share/elasticsearch/config/certs/", + 555 + ), + "/usr/share/elasticsearch/generate-certs.sh" + ) + // because we need to generate the certificates before Elasticsearch starts, the entry command has to be tuned accordingly + .withCommand( + "sh", + "-c", + "/usr/share/elasticsearch/generate-certs.sh && /usr/local/bin/docker-entrypoint.sh" + ) + .withCertPath(customizedCertPath) + ) { + container.start(); + assertClusterHealthResponse(container); + } + } + @Test public void testElasticsearchDefaultMaxHeapSize() throws Exception { long defaultHeapSize = 2147483648L;