Skip to content

Commit

Permalink
added opensearch container (#1497)
Browse files Browse the repository at this point in the history
Co-authored-by: dzhdanov <dzhdanov@playtika.com>
  • Loading branch information
zhdanowd and dzhdanov authored Sep 19, 2023
1 parent 7b3b773 commit 33dfcd4
Show file tree
Hide file tree
Showing 22 changed files with 539 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ embedded:

=== link:embedded-elasticsearch/README.adoc[embedded-elasticsearch]

=== link:embedded-opensearch/README.adoc[embedded-opensearch]

=== link:embedded-dynamodb/README.adoc[embedded-dynamodb]

=== link:embedded-voltdb/README.adoc[embedded-voltdb]
Expand Down
48 changes: 48 additions & 0 deletions embedded-opensearch/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
=== embedded-opensearch

==== Maven dependency

.pom.xml
[source,xml]
----
<dependency>
<groupId>com.playtika.testcontainers</groupId>
<artifactId>embedded-opensearch</artifactId>
<scope>test</scope>
</dependency>
----

==== Consumes (via `bootstrap.properties`)

* `embedded.opensearch.enabled` `(true|false, default is true)`
* `embedded.opensearch.reuseContainer` `(true|false, default is false)`
* `embedded.opensearch.dockerImage` `(default is 'opensearchproject/opensearch:2.9.0')`
** Image versions on https://hub.docker.com/r/opensearchproject/opensearch[docker.opensearch.co]
* `embedded.opensearch.indices` `(indices to create, no indices are created by default)`
* `embedded.opensearch.waitTimeoutInSeconds` `(default is 60 seconds)`
* `embedded.toxiproxy.proxies.opensearch.enabled` Enables both creation of the container with ToxiProxy TCP proxy and a proxy to the `embedded-opensearch` container.


==== Produces

* `embedded.opensearch.clusterName`
* `embedded.opensearch.host`
* `embedded.opensearch.httpPort`
* `embedded.opensearch.transportPort`
* `embedded.opensearch.toxiproxy.host`
* `embedded.opensearch.toxiproxy.port`
* `embedded.opensearch.networkAlias`
* `embedded.opensearch.internalHttpPort`
* `embedded.opensearch.internalTransportPort`
* Bean `ToxiproxyContainer.ContainerProxy opensearchContainerProxy`


==== Example

To use autoconfigured opensearch client use this property in your test `application.properties`:

./src/test/resources/application.properties
[source,properties]
----
opensearch.uris=http://${embedded.opensearch.host}:${embedded.opensearch.httpPort}
----
46 changes: 46 additions & 0 deletions embedded-opensearch/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>testcontainers-spring-boot-parent</artifactId>
<groupId>com.playtika.testcontainers</groupId>
<version>3.0.1</version>
<relativePath>../testcontainers-spring-boot-parent</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>embedded-opensearch</artifactId>

<dependencies>
<dependency>
<groupId>com.playtika.testcontainers</groupId>
<artifactId>testcontainers-common</artifactId>
</dependency>
<dependency>
<groupId>com.playtika.testcontainers</groupId>
<artifactId>embedded-toxiproxy</artifactId>
</dependency>
<dependency>
<groupId>org.opensearch</groupId>
<artifactId>opensearch-testcontainers</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.opensearch.client</groupId>
<artifactId>spring-data-opensearch-starter</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.playtika.testcontainer.opensearch;

import com.playtika.testcontainer.common.spring.DockerPresenceBootstrapConfiguration;
import com.playtika.testcontainer.toxiproxy.condition.ConditionalOnToxiProxyEnabled;
import lombok.extern.slf4j.Slf4j;
import org.opensearch.testcontainers.OpensearchContainer;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.ToxiproxyContainer;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

import static com.playtika.testcontainer.common.utils.ContainerUtils.configureCommonsAndStart;
import static com.playtika.testcontainer.opensearch.OpenSearchProperties.BEAN_NAME_EMBEDDED_OPEN_SEARCH;

@Slf4j
@Configuration
@ConditionalOnExpression("${embedded.containers.enabled:true}")
@AutoConfigureAfter(DockerPresenceBootstrapConfiguration.class)
@ConditionalOnProperty(name = "embedded.opensearch.enabled", matchIfMissing = true)
@EnableConfigurationProperties(OpenSearchProperties.class)
public class EmbeddedOpenSearchBootstrapConfiguration {

private static final String OPENSEARCH_NETWORK_ALIAS = "opensearch.testcontainer.docker";

@Bean
@ConditionalOnToxiProxyEnabled(module = "opensearch")
ToxiproxyContainer.ContainerProxy opensearchContainerProxy(ToxiproxyContainer toxiproxyContainer,
@Qualifier(BEAN_NAME_EMBEDDED_OPEN_SEARCH) OpensearchContainer opensearch,
OpenSearchProperties properties,
ConfigurableEnvironment environment) {
ToxiproxyContainer.ContainerProxy proxy = toxiproxyContainer.getProxy(opensearch, properties.getHttpPort());

Map<String, Object> map = new LinkedHashMap<>();
map.put("embedded.opensearch.toxiproxy.host", proxy.getContainerIpAddress());
map.put("embedded.opensearch.toxiproxy.port", proxy.getProxyPort());
map.put("embedded.opensearch.toxiproxy.proxyName", proxy.getName());

MapPropertySource propertySource = new MapPropertySource("embeddedOpenSearchToxiproxyInfo", map);
environment.getPropertySources().addFirst(propertySource);
log.info("Started OpenSearch ToxiProxy connection details {}", map);

return proxy;
}

@ConditionalOnMissingBean(name = BEAN_NAME_EMBEDDED_OPEN_SEARCH)
@Bean(name = BEAN_NAME_EMBEDDED_OPEN_SEARCH, destroyMethod = "stop")
public OpensearchContainer openSearch(ConfigurableEnvironment environment,
OpenSearchProperties properties,
Optional<Network> network) {

OpensearchContainer openSearch = OpenSearchContainerFactory.create(properties)
.withNetworkAliases(OPENSEARCH_NETWORK_ALIAS);
network.ifPresent(openSearch::withNetwork);
openSearch = (OpensearchContainer) configureCommonsAndStart(openSearch, properties, log);
registerOpenSearchEnvironment(openSearch, environment, properties);
return openSearch;
}

private void registerOpenSearchEnvironment(OpensearchContainer OpenSearch,
ConfigurableEnvironment environment,
OpenSearchProperties properties) {
Integer httpPort = OpenSearch.getMappedPort(properties.getHttpPort());
Integer transportPort = OpenSearch.getMappedPort(properties.getTransportPort());
String host = OpenSearch.getHost();

LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("embedded.opensearch.clusterName", properties.getClusterName());
map.put("embedded.opensearch.host", host);
map.put("embedded.opensearch.httpPort", httpPort);
map.put("embedded.opensearch.transportPort", transportPort);
map.put("embedded.opensearch.networkAlias", OPENSEARCH_NETWORK_ALIAS);
map.put("embedded.opensearch.internalHttpPort", properties.getHttpPort());
map.put("embedded.opensearch.internalTransportPort", properties.getTransportPort());

log.info("Started OpenSearch server. Connection details: {}", map);

MapPropertySource propertySource = new MapPropertySource("embeddedOpenSearchInfo", map);
environment.getPropertySources().addFirst(propertySource);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.playtika.testcontainer.opensearch;

import com.playtika.testcontainer.common.spring.DependsOnPostProcessor;
import org.opensearch.client.RestClient;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;

import static com.playtika.testcontainer.opensearch.OpenSearchProperties.BEAN_NAME_EMBEDDED_OPEN_SEARCH;

@AutoConfiguration(afterName = "org.opensearch.spring.boot.autoconfigure.OpenSearchRestClientAutoConfiguration")
@AutoConfigureOrder
@ConditionalOnExpression("${embedded.containers.enabled:true}")
@ConditionalOnClass(RestClient.class)
@ConditionalOnProperty(name = "embedded.opensearch.enabled", matchIfMissing = true)
public class EmbeddedOpenSearchRestClientDependenciesAutoConfiguration {

@Bean
public static BeanFactoryPostProcessor opensearchRestClientDependencyPostProcessor() {
return new DependsOnPostProcessor(RestClient.class, new String[]{BEAN_NAME_EMBEDDED_OPEN_SEARCH});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.playtika.testcontainer.opensearch;

import com.playtika.testcontainer.common.utils.ContainerUtils;
import com.playtika.testcontainer.opensearch.rest.CreateIndex;
import com.playtika.testcontainer.opensearch.rest.WaitForGreenStatus;
import org.opensearch.testcontainers.OpensearchContainer;
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
import org.testcontainers.containers.wait.strategy.WaitStrategy;

class OpenSearchContainerFactory {

static OpensearchContainer create(OpenSearchProperties properties) {
return new OpensearchContainer(ContainerUtils.getDockerImageName(properties))
.withExposedPorts(properties.httpPort, properties.transportPort)
.withEnv("cluster.name", properties.getClusterName())
.withEnv("discovery.type", "single-node")
.withEnv("ES_JAVA_OPTS", getJavaOpts(properties))
.waitingFor(getCompositeWaitStrategy(properties));
}

private static String getJavaOpts(OpenSearchProperties properties) {
return "-Xms" + properties.getClusterRamMb() + "m -Xmx" + properties.getClusterRamMb() + "m";
}

private static WaitStrategy getCompositeWaitStrategy(OpenSearchProperties properties) {
WaitAllStrategy strategy = new WaitAllStrategy()
.withStrategy(new HostPortWaitStrategy());
properties.indices.forEach(index -> strategy.withStrategy(new CreateIndex(properties, index)));
return strategy
.withStrategy(new WaitForGreenStatus(properties))
.withStartupTimeout(properties.getTimeoutDuration());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.playtika.testcontainer.opensearch;

import com.playtika.testcontainer.common.properties.CommonContainerProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.ArrayList;
import java.util.List;

@Data
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties("embedded.opensearch")
public class OpenSearchProperties extends CommonContainerProperties {
public static final String BEAN_NAME_EMBEDDED_OPEN_SEARCH = "embeddedOpenSearch";

String clusterName = "test_cluster";
String host = "localhost";
List<String> indices = new ArrayList<>();
int httpPort = 9200;
int transportPort = 9300;
int clusterRamMb = 256;

// https://hub.docker.com/r/opensearchproject/opensearch
@Override
public String getDefaultDockerImage() {
// Please don`t remove this comment.
// renovate: datasource=docker
return "opensearchproject/opensearch:2.9.0";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.playtika.testcontainer.opensearch.rest;

import com.playtika.testcontainer.common.checks.AbstractInitOnStartupStrategy;
import com.playtika.testcontainer.opensearch.OpenSearchProperties;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class CreateIndex extends AbstractInitOnStartupStrategy {

private final OpenSearchProperties properties;
private final String index;

@Override
public String[] getScriptToExecute() {
return new String[]{
"curl", "-X", "PUT",
"http://127.0.0.1:" + properties.getHttpPort() + "/" + index
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.playtika.testcontainer.opensearch.rest;

import com.playtika.testcontainer.common.checks.AbstractCommandWaitStrategy;
import com.playtika.testcontainer.opensearch.OpenSearchProperties;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class WaitForGreenStatus extends AbstractCommandWaitStrategy {

private final OpenSearchProperties properties;

@Override
public String[] getCheckCommand() {
return new String[]{
"curl", "-X", "GET",
"http://127.0.0.1:" + properties.getHttpPort() + "/_cluster/health?wait_for_status=green&timeout=10s"
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"groups": [
],
"properties": [
{
"name": "embedded.opensearch.enabled",
"type": "java.lang.Boolean",
"defaultValue": "true"
}
],
"hints": [
{
"name": "embedded.opensearch.enabled",
"values": [
{
"value": "true",
"description": "Enables configuration of OpenSearch server on startup."
},
{
"value": "false",
"description": "Disables configuration of OpenSearch server on startup."
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.playtika.testcontainer.opensearch.EmbeddedOpenSearchBootstrapConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.playtika.testcontainer.opensearch.EmbeddedOpenSearchRestClientDependenciesAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.playtika.testcontainer.opensearch;

import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.testcontainers.containers.Container;

import static org.assertj.core.api.Assertions.assertThat;

public class DisabledOpenSearchTest {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
EmbeddedOpenSearchBootstrapConfiguration.class,
EmbeddedOpenSearchRestClientDependenciesAutoConfiguration.class));

@Test
public void contextLoads() {
contextRunner
.withPropertyValues(
"embedded.opensearch.enabled=false"
)
.run((context) -> assertThat(context)
.hasNotFailed()
.doesNotHaveBean(Container.class)
.doesNotHaveBean("openClientDependencyPostProcessor"));
}

}
Loading

0 comments on commit 33dfcd4

Please sign in to comment.