diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/elasticsearch/DataElasticsearchTestVer8IntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/elasticsearch/DataElasticsearchTestVer8IntegrationTests.java new file mode 100644 index 000000000000..4ff57d486d8f --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/elasticsearch/DataElasticsearchTestVer8IntegrationTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.elasticsearch; + +import java.time.Duration; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.testcontainers.elasticsearch.ElasticsearchContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testcontainers.service.connection.ServiceConnectionAutoConfiguration; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.ApplicationContext; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.boot.test.autoconfigure.AutoConfigurationImportedCondition.importedAutoConfiguration; + +/** + * Sample test for {@link DataElasticsearchTest @DataElasticsearchTest} + * + * @author EddĂș MelĂ©ndez + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Piotr Przybyl + */ +@DataElasticsearchTest +@Testcontainers(disabledWithoutDocker = true) +class DataElasticsearchTestVer8IntegrationTests { + + @Container + @ServiceConnection + static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch8()) + .withEnv("ES_JAVA_OPTS", "-Xms32m -Xmx512m") + .withStartupAttempts(5) + .withStartupTimeout(Duration.ofMinutes(10)); + + @Autowired + private ElasticsearchTemplate elasticsearchTemplate; + + @Autowired + private ExampleRepository exampleRepository; + + @Autowired + private ApplicationContext applicationContext; + + @DynamicPropertySource + static void configureSsl(DynamicPropertyRegistry registry) { + elasticsearch.caCertAsBytes().ifPresent(caBytes -> { + String bundleName = "esContainer" + elasticsearch.getContainerId(); + registry.add("spring.elasticsearch.restclient.ssl.bundle", () -> bundleName); + registry.add("spring.ssl.bundle.pem." + bundleName + ".truststore.certificate", () -> new String(caBytes)); + }); + } + + @Test + void didNotInjectExampleService() { + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> this.applicationContext.getBean(ExampleService.class)); + } + + @Test + void testRepository() { + ExampleDocument document = new ExampleDocument(); + document.setText("Look, new @DataElasticsearchTest!"); + String id = UUID.randomUUID().toString(); + document.setId(id); + ExampleDocument savedDocument = this.exampleRepository.save(document); + ExampleDocument getDocument = this.elasticsearchTemplate.get(id, ExampleDocument.class); + assertThat(getDocument).isNotNull(); + assertThat(getDocument.getId()).isNotNull(); + assertThat(getDocument.getId()).isEqualTo(savedDocument.getId()); + this.exampleRepository.deleteAll(); + } + + @Test + void serviceConnectionAutoConfigurationWasImported() { + assertThat(this.applicationContext).has(importedAutoConfiguration(ServiceConnectionAutoConfiguration.class)); + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/elasticsearch/ElasticsearchContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/elasticsearch/ElasticsearchContainerConnectionDetailsFactory.java index eb2719a0262b..aaa11d94b901 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/elasticsearch/ElasticsearchContainerConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/elasticsearch/ElasticsearchContainerConnectionDetailsFactory.java @@ -18,6 +18,7 @@ import java.util.List; +import org.testcontainers.containers.Container.ExecResult; import org.testcontainers.elasticsearch.ElasticsearchContainer; import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails; @@ -25,6 +26,7 @@ import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.util.StringUtils; /** * {@link ContainerConnectionDetailsFactory} to create @@ -34,12 +36,19 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Piotr Przybyl */ class ElasticsearchContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory { private static final int DEFAULT_PORT = 9200; + private static final String DEFAULT_USERNAME = "elastic"; + + private static final String DEFAULT_PASSWORD = "changeme"; + + private static final String ELASTIC_PASSWORD_ENV_NAME = "ELASTIC_PASSWORD"; + @Override protected ElasticsearchConnectionDetails getContainerConnectionDetails( ContainerConnectionSource source) { @@ -53,6 +62,10 @@ protected ElasticsearchConnectionDetails getContainerConnectionDetails( private static final class ElasticsearchContainerConnectionDetails extends ContainerConnectionDetails implements ElasticsearchConnectionDetails { + private Boolean securityEnabled; + + private Boolean sslEnabled; + private ElasticsearchContainerConnectionDetails(ContainerConnectionSource source) { super(source); } @@ -61,9 +74,87 @@ private ElasticsearchContainerConnectionDetails(ContainerConnectionSource getNodes() { String host = getContainer().getHost(); Integer port = getContainer().getMappedPort(DEFAULT_PORT); - return List.of(new Node(host, port, Protocol.HTTP, null, null)); + return List.of(new Node(host, port, isSslEnabled() ? Protocol.HTTPS : Protocol.HTTP, getUsername(), getPassword())); } - } + @Override + public String getUsername() { + if (isSecurityEnabled()) { + return DEFAULT_USERNAME; + } + return ElasticsearchConnectionDetails.super.getUsername(); + } + @Override + public String getPassword() { + if (isSecurityEnabled()) { + String envPassword = getContainer().getEnvMap().get(ELASTIC_PASSWORD_ENV_NAME); + if (StringUtils.hasText(envPassword)) { + return envPassword; + } + return DEFAULT_PASSWORD; + } + return ElasticsearchConnectionDetails.super.getPassword(); + } + + private boolean isSslEnabled() { + // this is basic memoization; no synchronization needed as the results don't change over time + if (this.sslEnabled != null) { + return this.sslEnabled; + } + ExecResult execResult; + try { + execResult = getContainer().execInContainer("/usr/bin/curl", "-k", "https://localhost:" + DEFAULT_PORT); + } + catch (Exception e) { + throw new RuntimeException(e); + } + return switch (execResult.getExitCode()) { + case 0 -> { + this.sslEnabled = Boolean.TRUE; + yield true; + } + case 35 -> { + this.sslEnabled = Boolean.FALSE; + yield false; + } + case 7 -> + throw new IllegalStateException("Elasticsearch isn't listening on port " + DEFAULT_PORT); + default -> + throw new IllegalStateException("Unexpected exit code [" + execResult.getExitCode() + "]"); + }; + } + + private boolean isSecurityEnabled() { + // this is basic memoization; no synchronization needed as the results don't change over time + if (this.securityEnabled != null) { + return this.securityEnabled; + } + ExecResult execResult; + try { + // this call will print the HTTP status code: if security is enabled, it gives 401 + execResult = getContainer().execInContainer( + "/usr/bin/curl", + "-s", "-o", "/dev/null/", + "-I", "-w", "%{http_code}", + "-k", + (isSslEnabled() ? "https" : "http") + "://localhost:" + DEFAULT_PORT); + } + catch (Exception e) { + throw new RuntimeException(e); + } + return switch (execResult.getStdout()) { + case "200" -> { + this.securityEnabled = Boolean.FALSE; + yield false; + } + case "401" -> { + this.securityEnabled = Boolean.TRUE; + yield true; + } + default -> + throw new IllegalStateException("Cannot determine if security is enabled for Elasticsearch"); + }; + } + } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/elasticsearch/Elasticsearch7ContainerConnectionDetailsFactoryIntegrationTest.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/elasticsearch/Elasticsearch7ContainerConnectionDetailsFactoryIntegrationTest.java new file mode 100644 index 000000000000..0b2e54663b0b --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/elasticsearch/Elasticsearch7ContainerConnectionDetailsFactoryIntegrationTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testcontainers.service.connection.elasticsearch; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.testcontainers.elasticsearch.ElasticsearchContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class Elasticsearch7ContainerConnectionDetailsFactoryIntegrationTest { + + @Container + @ServiceConnection + static final ElasticsearchContainer esContainer = + new ElasticsearchContainer(DockerImageNames.elasticsearch()) + .withEnv("xpack.security.enabled", "true") + .withPassword("correct horse battery staple"); + + @Autowired + private ElasticsearchConnectionDetails connectionDetails; + + @Test + void connectionDetailsShouldBeSet() { + assertThat(this.connectionDetails).isNotNull(); + assertThat(this.connectionDetails.getPassword()).isEqualTo("correct horse battery staple"); + assertThat(this.connectionDetails.getUsername()).isEqualTo("elastic"); + List nodes = this.connectionDetails.getNodes(); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).protocol()).isEqualTo(Protocol.HTTP); + assertThat(nodes.get(0).port()).isEqualTo(esContainer.getMappedPort(9200)); + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/elasticsearch/Elasticsearch8ContainerConnectionDetailsFactoryIntegrationTest.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/elasticsearch/Elasticsearch8ContainerConnectionDetailsFactoryIntegrationTest.java new file mode 100644 index 000000000000..86aed042abf4 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/elasticsearch/Elasticsearch8ContainerConnectionDetailsFactoryIntegrationTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testcontainers.service.connection.elasticsearch; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.testcontainers.elasticsearch.ElasticsearchContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class Elasticsearch8ContainerConnectionDetailsFactoryIntegrationTest { + + @Container + @ServiceConnection + static final ElasticsearchContainer esContainer = + new ElasticsearchContainer(DockerImageNames.elasticsearch8()); + + @Autowired + private ElasticsearchConnectionDetails connectionDetails; + + @Test + void connectionDetailsShouldBeSet() { + assertThat(this.connectionDetails).isNotNull(); + assertThat(this.connectionDetails.getPassword()).isEqualTo("changeme"); + assertThat(this.connectionDetails.getUsername()).isEqualTo("elastic"); + List nodes = this.connectionDetails.getNodes(); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).protocol()).isEqualTo(Protocol.HTTPS); + assertThat(nodes.get(0).port()).isEqualTo(esContainer.getMappedPort(9200)); + } +} \ No newline at end of file