Skip to content

Added detecting protocol, username and password in ElasticsearchConta… #39092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@

import java.util.List;

import org.testcontainers.containers.Container.ExecResult;
import org.testcontainers.elasticsearch.ElasticsearchContainer;

import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol;
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
Expand All @@ -34,12 +36,19 @@
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Piotr Przybyl
*/
class ElasticsearchContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<ElasticsearchContainer, ElasticsearchConnectionDetails> {

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<ElasticsearchContainer> source) {
Expand All @@ -53,6 +62,10 @@ protected ElasticsearchConnectionDetails getContainerConnectionDetails(
private static final class ElasticsearchContainerConnectionDetails
extends ContainerConnectionDetails<ElasticsearchContainer> implements ElasticsearchConnectionDetails {

private Boolean securityEnabled;

private Boolean sslEnabled;

private ElasticsearchContainerConnectionDetails(ContainerConnectionSource<ElasticsearchContainer> source) {
super(source);
}
Expand All @@ -61,9 +74,87 @@ private ElasticsearchContainerConnectionDetails(ContainerConnectionSource<Elasti
public List<Node> 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");
};
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Node> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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<Node> 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));
}
}