blob: f742b32cbe5d260420b328753272989274585e7e [file] [log] [blame]
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you 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 *
* *
* http://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.apache.james.backends.opensearch;
import static java.net.HttpURLConnection.HTTP_OK;
import static org.apache.james.backends.opensearch.DockerOpenSearch.Fixture.OS_HTTP_PORT;
import static org.apache.james.backends.opensearch.DockerOpenSearch.Fixture.OS_MEMORY;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.HttpStatus;
import org.apache.james.backends.opensearch.OpenSearchConfiguration.Credential;
import org.apache.james.backends.opensearch.OpenSearchConfiguration.HostScheme;
import org.apache.james.util.Host;
import org.apache.james.util.docker.DockerContainer;
import org.apache.james.util.docker.Images;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.containers.wait.strategy.WaitStrategy;
import org.testcontainers.images.builder.ImageFromDockerfile;
import com.google.common.collect.ImmutableMap;
import feign.Client;
import feign.Feign;
import feign.Logger;
import feign.RequestLine;
import feign.Response;
import feign.auth.BasicAuthRequestInterceptor;
import feign.slf4j.Slf4jLogger;
public interface DockerOpenSearch {
interface OpenSearchAPI {
class Builder {
private static final HostnameVerifier ACCEPT_ANY_HOST = (hostname, sslSession) -> true;
private static final TrustManager[] TRUST_ALL = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}
};
private final Feign.Builder requestBuilder;
private final URL esURL;
public Builder(URL esURL) {
this.esURL = esURL;
this.requestBuilder = Feign.builder()
.logger(new Slf4jLogger(OpenSearchAPI.class))
.logLevel(Logger.Level.FULL);
}
public Builder credential(Credential credential) {
requestBuilder.requestInterceptor(
new BasicAuthRequestInterceptor(credential.getUsername(), String.valueOf(credential.getPassword())));
return this;
}
public Builder disableSSLValidation() throws Exception {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, TRUST_ALL, new java.security.SecureRandom());
SSLSocketFactory factory = sc.getSocketFactory();
HttpsURLConnection.setDefaultSSLSocketFactory(factory);
Client ignoredSSLClient = new Client.Default(factory, ACCEPT_ANY_HOST);
requestBuilder.client(ignoredSSLClient);
return this;
}
public OpenSearchAPI build() {
return requestBuilder.target(OpenSearchAPI.class, esURL.toString());
}
}
static Builder builder(URL esURL) {
return new Builder(esURL);
}
@RequestLine("DELETE /_all")
Response deleteAllIndexes();
@RequestLine("POST /_flush?force&wait_if_ongoing=true")
Response flush();
}
interface Fixture {
int OS_HTTP_PORT = 9200;
int OS_MEMORY = 1024;
}
class NoAuth implements DockerOpenSearch {
private static WaitStrategy openSearchWaitStrategy() {
return new HttpWaitStrategy()
.forPort(OS_HTTP_PORT)
.forStatusCodeMatching(response -> response == HTTP_OK)
.withReadTimeout(Duration.ofSeconds(10))
.withStartupTimeout(Duration.ofMinutes(3));
}
static DockerContainer defaultContainer(String imageName) {
return DockerContainer.fromName(imageName)
.withTmpFs(ImmutableMap.of("/usr/share/opensearch/data/nodes/0", "rw,size=200m"))
.withExposedPorts(OS_HTTP_PORT)
.withEnv("discovery.type", "single-node")
.withEnv("DISABLE_INSTALL_DEMO_CONFIG", "true")
.withEnv("DISABLE_SECURITY_PLUGIN", "true")
.withEnv("OPENSEARCH_JAVA_OPTS", "-Xms" + OS_MEMORY + "m -Xmx" + OS_MEMORY + "m")
.withAffinityToContainer()
.withName("james-testing-opensearch-" + UUID.randomUUID())
.waitingFor(openSearchWaitStrategy());
}
private final DockerContainer eSContainer;
public NoAuth() {
this(Images.OPENSEARCH);
}
public NoAuth(String imageName) {
this.eSContainer = defaultContainer(imageName);
}
public NoAuth(DockerContainer eSContainer) {
this.eSContainer = eSContainer;
}
public void start() {
if (!isRunning()) {
eSContainer.start();
}
}
public void stop() {
eSContainer.stop();
}
public int getHttpPort() {
return eSContainer.getMappedPort(OS_HTTP_PORT);
}
public String getIp() {
return eSContainer.getHostIp();
}
public Host getHttpHost() {
return Host.from(getIp(), getHttpPort());
}
public void pause() {
eSContainer.pause();
}
public void unpause() {
eSContainer.unpause();
}
@Override
public boolean isRunning() {
return eSContainer.isRunning();
}
}
class WithAuth implements DockerOpenSearch {
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(WithAuth.class);
private static final String DEFAULT_USERNAME = "elasticsearch";
private static final String DEFAULT_PASSWORD = "secret";
public static final Credential DEFAULT_CREDENTIAL =
Credential.of(DEFAULT_USERNAME, DEFAULT_PASSWORD);
private final DockerOpenSearch.NoAuth openSearch;
private final DockerContainer nginx;
private final Network network;
public WithAuth() {
this(Images.OPENSEARCH);
}
WithAuth(String imageName) {
this.network = Network.newNetwork();
this.openSearch = new DockerOpenSearch.NoAuth(
DockerOpenSearch.NoAuth
.defaultContainer(imageName)
.withLogConsumer(frame -> LOGGER.debug("[OpenSearch] " + frame.getUtf8String()))
.withNetwork(network)
.withNetworkAliases("elasticsearch"));
this.nginx = new DockerContainer(
new GenericContainer<>(
new ImageFromDockerfile()
.withFileFromClasspath("conf/nginx-conf/", "auth-es/nginx-conf/")
.withFileFromClasspath("conf/default.crt", "auth-es/default.crt")
.withFileFromClasspath("conf/default.key", "auth-es/default.key")
.withFileFromClasspath("Dockerfile", "auth-es/NginxDockerfile")))
.withExposedPorts(OS_HTTP_PORT)
.withTmpFs(ImmutableMap.of("/usr/share/opensearch/data/nodes/0", "rw,size=200m"))
.withLogConsumer(frame -> LOGGER.debug("[NGINX] " + frame.getUtf8String()))
.withNetwork(network)
.withName("james-testing-nginx-" + UUID.randomUUID());
}
public void start() {
openSearch.start();
nginx.start();
}
public void stop() {
nginx.stop();
openSearch.stop();
}
public int getHttpPort() {
return nginx.getMappedPort(OS_HTTP_PORT);
}
public String getIp() {
return nginx.getHostIp();
}
@Override
public OpenSearchAPI esAPI() {
try {
return OpenSearchAPI.builder(getUrl())
.credential(DEFAULT_CREDENTIAL)
.disableSSLValidation()
.build();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public URL getUrl() {
try {
return new URI("https://" + getIp() + ":" + getHttpPort()).toURL();
} catch (MalformedURLException | URISyntaxException e) {
throw new RuntimeException(e);
}
}
@Override
public OpenSearchConfiguration configuration(Optional<Duration> requestTimeout) {
return configurationBuilder(requestTimeout)
.hostScheme(Optional.of(HostScheme.HTTPS))
.build();
}
public void pause() {
nginx.pause();
openSearch.pause();
}
public void unpause() {
openSearch.unpause();
nginx.unpause();
}
@Override
public boolean isRunning() {
return nginx.isRunning() && openSearch.isRunning();
}
}
void start();
void stop();
int getHttpPort();
String getIp();
void pause();
void unpause();
boolean isRunning();
default URL getUrl() {
try {
return new URI("http://" + getIp() + ":" + getHttpPort()).toURL();
} catch (MalformedURLException | URISyntaxException e) {
throw new RuntimeException(e);
}
}
default Host getHttpHost() {
return Host.from(getIp(), getHttpPort());
}
default void cleanUpData() {
if (esAPI().deleteAllIndexes().status() != HttpStatus.SC_OK) {
throw new IllegalStateException("Failed to delete all data from OpenSearch");
}
}
default void flushIndices() {
if (esAPI().flush().status() != HttpStatus.SC_OK) {
throw new IllegalStateException("Failed to flush OpenSearch");
}
}
default OpenSearchConfiguration configuration(Optional<Duration> requestTimeout) {
return configurationBuilder(requestTimeout)
.build();
}
default OpenSearchConfiguration.Builder configurationBuilder(Optional<Duration> requestTimeout) {
return OpenSearchConfiguration.builder()
.addHost(getHttpHost())
.requestTimeout(requestTimeout)
.nbReplica(0)
.nbShards(1);
}
default OpenSearchConfiguration configuration() {
return configuration(Optional.empty());
}
default ClientProvider clientProvider() {
return new ClientProvider(configuration(Optional.empty()));
}
default ClientProvider clientProvider(Duration requestTimeout) {
return new ClientProvider(configuration(Optional.of(requestTimeout)));
}
default OpenSearchAPI esAPI() {
return OpenSearchAPI.builder(getUrl())
.build();
}
}