/*
* This file is part of ReadonlyREST.
*
* ReadonlyREST is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ReadonlyREST is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ReadonlyREST. If not, see http://www.gnu.org/licenses/
*/
package org.elasticsearch.plugin.readonlyrest.utils.containers;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.plugin.readonlyrest.utils.Tuple;
import org.elasticsearch.plugin.readonlyrest.utils.containers.exceptions.ContainerCreationException;
import org.elasticsearch.plugin.readonlyrest.utils.gradle.GradleProjectUtils;
import org.elasticsearch.plugin.readonlyrest.utils.gradle.GradleProperties;
import org.elasticsearch.plugin.readonlyrest.utils.httpclient.RestClient;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.WaitStrategy;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.shaded.com.fasterxml.jackson.core.type.TypeReference;
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import static org.elasticsearch.plugin.readonlyrest.utils.containers.ContainerUtils.checkTimeout;
public class ESWithReadonlyRestContainer extends GenericContainer<ESWithReadonlyRestContainer> {
private static Logger logger = LogManager.getLogger(ESWithReadonlyRestContainer.class);
private static int ES_PORT = 9200;
private static Duration WAIT_BETWEEN_RETRIES = Duration.ofSeconds(1);
private static Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(240);
private static String ADMIN_LOGIN = "admin";
private static String ADMIN_PASSWORD = "container";
private static GradleProperties properties =
GradleProperties
.create()
.orElseThrow(() -> new ContainerCreationException("Cannot load gradle properties"));
private ESWithReadonlyRestContainer(ImageFromDockerfile imageFromDockerfile) {
super(imageFromDockerfile);
}
public static ESWithReadonlyRestContainer create(String elasticsearchConfig,
Optional<ESWithReadonlyRestContainer.ESInitalizer> initalizer) {
File config = ContainerUtils.getResourceFile(elasticsearchConfig);
Optional<File> pluginFileOpt = GradleProjectUtils.assemble();
if (!pluginFileOpt.isPresent()) {
throw new ContainerCreationException("Plugin file assembly failed");
}
File pluginFile = pluginFileOpt.get();
logger.info("Creating ES container ...");
String elasticsearchConfigName = "elasticsearch.yml";
ESWithReadonlyRestContainer container = new ESWithReadonlyRestContainer(
new ImageFromDockerfile()
.withFileFromFile(pluginFile.getName(), pluginFile)
.withFileFromFile(elasticsearchConfigName, config)
.withDockerfileFromBuilder(builder -> builder
.from("docker.elastic.co/elasticsearch/elasticsearch:" + properties.getProperty("esVersion"))
.copy(pluginFile.getName(), "/tmp/")
.copy(elasticsearchConfigName, "/usr/share/elasticsearch/config/")
.run("yes | /usr/share/elasticsearch/bin/elasticsearch-plugin install " +
"file:/tmp/" + pluginFile.getName())
.build()));
return container
.withLogConsumer((l) -> System.out.print(l.getUtf8String()))
.withExposedPorts(ES_PORT)
.waitingFor(container.waitStrategy(initalizer).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT));
}
public String getESHost() {
return this.getContainerIpAddress();
}
public Integer getESPort() {
return this.getMappedPort(ES_PORT);
}
public RestClient getClient(Header... headers) {
return new RestClient(getESHost(), getESPort(), Optional.empty(), headers);
}
public RestClient getBasicAuthClient(String name, String password) {
return new RestClient(getESHost(), getESPort(), Optional.of(Tuple.from(name, password)));
}
private RestClient getAdminClient() {
return new RestClient(getESHost(), getESPort(), Optional.of(Tuple.from(ADMIN_LOGIN, ADMIN_PASSWORD)));
}
private WaitStrategy waitStrategy(Optional<ESWithReadonlyRestContainer.ESInitalizer> initalizer) {
final ObjectMapper mapper = new ObjectMapper();
return new GenericContainer.AbstractWaitStrategy() {
@Override
protected void waitUntilReady() {
logger.info("Waiting for ES container ...");
final RestClient client = getAdminClient();
final Instant startTime = Instant.now();
while (!isReady(client) && !checkTimeout(startTime, startupTimeout)) {
try {
Thread.sleep(WAIT_BETWEEN_RETRIES.toMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
initalizer.ifPresent(i -> i.initialize(client));
logger.info("ES container stated");
}
private boolean isReady(RestClient client) {
try {
HttpResponse result = client.execute(new HttpGet(client.from("_cluster/health")));
if (result.getStatusLine().getStatusCode() != 200) return false;
Map<String, String> healthJson = mapper.readValue(
result.getEntity().getContent(),
new TypeReference<Map<String, String>>() {
}
);
return "green".equals(healthJson.get("status"));
} catch (IOException | URISyntaxException e) {
return false;
}
}
};
}
public interface ESInitalizer {
void initialize(RestClient client);
}
}