Skip to content
Merged
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
Expand Up @@ -6,6 +6,8 @@
import org.apache.commons.lang3.StringUtils;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.ComparableVersion;
import org.testcontainers.utility.DockerImageName;
Expand All @@ -15,6 +17,8 @@
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;

import javax.net.ssl.SSLContext;
Expand Down Expand Up @@ -75,6 +79,8 @@ public class ElasticsearchContainer extends GenericContainer<ElasticsearchContai

private String certPath = "";

private Duration healthCheckTimeout = Duration.ofSeconds(60);

/**
* Create an Elasticsearch Container by passing the full docker image name
*
Expand Down Expand Up @@ -113,15 +119,11 @@ public ElasticsearchContainer(final DockerImageName dockerImageName) {
BindMode.READ_ONLY
);
addExposedPorts(ELASTICSEARCH_DEFAULT_PORT, ELASTICSEARCH_DEFAULT_TCP_PORT);
this.isAtLeastMajorVersion8 =
new ComparableVersion(dockerImageName.getVersionPart()).isGreaterThanOrEqualTo("8.0.0");
// regex that
// matches 8.3 JSON logging with started message and some follow up content within the message field
// matches 8.0 JSON logging with no whitespace between message field and content
// matches 7.x JSON logging with whitespace between message field and content
// matches 6.x text logging with node name in brackets and just a 'started' message till the end of the line
String regex = ".*(\"message\":\\s?\"started[\\s?|\"].*|] started\n$)";
setWaitStrategy(Wait.forLogMessage(regex, 1));
String versionPart = dockerImageName.getVersionPart();
this.isAtLeastMajorVersion8 = new ComparableVersion(versionPart).isGreaterThanOrEqualTo("8.0.0");
// Wait strategy is deferred to configure() so it can read the final env map
// (e.g. password and SSL settings that the user may set after construction).
setWaitStrategy(null);
if (isAtLeastMajorVersion8) {
withPassword(ELASTICSEARCH_DEFAULT_PASSWORD);
withCertPath(DEFAULT_CERT_PATH);
Expand Down Expand Up @@ -209,17 +211,67 @@ public ElasticsearchContainer withCertPath(String certPath) {
return this;
}

@Override
public ElasticsearchContainer withStartupTimeout(Duration startupTimeout) {
this.healthCheckTimeout = startupTimeout;
if (getWaitStrategy() != null) {
getWaitStrategy().withStartupTimeout(startupTimeout);
}
return self();
}

String getCertPath() {
return certPath;
}

@Override
protected void configure() {
super.configure();
configureWaitStrategy();
}

private void configureWaitStrategy() {
if (getWaitStrategy() != null) {
return;
}
setWaitStrategy(
new AbstractWaitStrategy() {
@Override
protected void waitUntilReady() {
String password = getEnvMap().get("ELASTIC_PASSWORD");
// Wait for port 9200 to accept TCP connections first, so that
// getHttpScheme()'s curl probe always finds a live socket and no
// version-based heuristics are needed.
// Track the deadline so both steps share the same total timeout.
Instant deadline = Instant.now().plus(startupTimeout);
Wait.forListeningPort().withStartupTimeout(startupTimeout).waitUntilReady(waitStrategyTarget);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Duration remaining = Duration.between(Instant.now(), deadline);
HttpWaitStrategy inner = "https".equals(getHttpScheme())
? Wait.forHttps("/_cluster/health").forPort(ELASTICSEARCH_DEFAULT_PORT).allowInsecure()
: Wait.forHttp("/_cluster/health").forPort(ELASTICSEARCH_DEFAULT_PORT);
if (password != null) {
inner = inner.withBasicCredentials("elastic", password);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
inner
.forStatusCode(200)
.forResponsePredicate(body -> {
return body.contains("\"status\":\"green\"") || body.contains("\"status\":\"yellow\"");
})
.withStartupTimeout(remaining.isNegative() ? Duration.ZERO : remaining)
.waitUntilReady(waitStrategyTarget);
}
}
.withStartupTimeout(healthCheckTimeout)
);
}

public String getHttpHostAddress() {
return getHost() + ":" + getMappedPort(ELASTICSEARCH_DEFAULT_PORT);
}

/**
* Checks env first if this implies HTTP/HTTPS.
* Otherwise, detects the scheme used by Elasticsearch using <code>curl</code>
* Detects the HTTP scheme used by Elasticsearch. Respects explicit env-var config first;
* when ambiguous, probes the live socket with curl (requires a running container on port 9200).
*
* @return "http" or "https"
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ class ElasticsearchContainerTest {
.parse("docker.elastic.co/elasticsearch/elasticsearch")
.withTag(ELASTICSEARCH_VERSION);

private static final DockerImageName ELASTICSEARCH_LATEST_IMAGE = DockerImageName.parse(
"docker.elastic.co/elasticsearch/elasticsearch:9.2.4"
);

/**
* Elasticsearch default username, when secured
*/
Expand Down Expand Up @@ -162,6 +166,23 @@ void elasticsearchVersion83() throws IOException {
}
}

@Test
void clusterHealthIsAtLeastYellowAfterStart() throws IOException {
try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_LATEST_IMAGE)) {
container.start();

Response response = getClient(container).performRequest(new Request("GET", "/_cluster/health"));
assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
String body = EntityUtils.toString(response.getEntity());
assertThat(body)
.as("Cluster health status should be at least yellow after container start")
.satisfiesAnyOf(
b -> assertThat(b).contains("\"status\":\"yellow\""),
b -> assertThat(b).contains("\"status\":\"green\"")
);
}
}

@Test
void elasticsearchOssImage() throws IOException {
try (
Expand Down
Loading