From 99a3ed54f28fcea2684a88cf2f1fb6fe97594fb7 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Mon, 24 Jul 2023 12:01:18 +0200 Subject: [PATCH] Add a way to configure an OpenSearch distribution for the Elasticsearch DevService --- bom/application/pom.xml | 7 ++ docs/src/main/asciidoc/_attributes.adoc | 1 + .../asciidoc/elasticsearch-dev-services.adoc | 15 ++- .../devservices/common/ConfigureUtil.java | 6 +- .../deployment/pom.xml | 4 + .../DevServicesElasticsearchProcessor.java | 115 ++++++++++++++---- .../DevservicesElasticsearchBuildItem.java | 8 +- ...asticsearchDevServicesBuildTimeConfig.java | 19 ++- ...HibernateSearchElasticsearchProcessor.java | 3 +- .../README.md | 4 +- .../hibernate-search-orm-opensearch/README.md | 4 +- ...ibernateSearchDevServicesTestResource.java | 71 +++++++++++ .../opensearch/devservices/IndexedEntity.java | 47 +++++++ .../src/main/resources/application.properties | 10 +- .../devservices/DevServicesContextSpy.java | 32 +++++ ...chDevServicesConfiguredExplicitlyTest.java | 85 +++++++++++++ ...archDevServicesDisabledExplicitlyTest.java | 75 ++++++++++++ ...archDevServicesDisabledImplicitlyTest.java | 73 +++++++++++ ...earchDevServicesEnabledImplicitlyTest.java | 83 +++++++++++++ 19 files changed, 620 insertions(+), 42 deletions(-) create mode 100644 integration-tests/hibernate-search-orm-opensearch/src/main/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchDevServicesTestResource.java create mode 100644 integration-tests/hibernate-search-orm-opensearch/src/main/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/IndexedEntity.java create mode 100644 integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/DevServicesContextSpy.java create mode 100644 integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesConfiguredExplicitlyTest.java create mode 100644 integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesDisabledExplicitlyTest.java create mode 100644 integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesDisabledImplicitlyTest.java create mode 100644 integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesEnabledImplicitlyTest.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 17ecbd8e08879..0fc068cc942e4 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -206,6 +206,8 @@ 0.1.17.Final 1.18.3 3.3.0 + + 2.0.0 1.3.0 2.7 2.4 @@ -382,6 +384,11 @@ + + org.opensearch + opensearch-testcontainers + ${opensearch-testcontainers.version} + diff --git a/docs/src/main/asciidoc/_attributes.adoc b/docs/src/main/asciidoc/_attributes.adoc index 610ab5295bd07..6f24e585d4d04 100644 --- a/docs/src/main/asciidoc/_attributes.adoc +++ b/docs/src/main/asciidoc/_attributes.adoc @@ -13,6 +13,7 @@ :gradle-version: ${gradle-wrapper.version} :elasticsearch-version: ${elasticsearch-server.version} :elasticsearch-image: ${elasticsearch.image} +:opensearch-image: ${opensearch.image} :infinispan-version: ${infinispan.version} :infinispan-protostream-version: ${infinispan.protostream.version} :logstash-image: ${logstash.image} diff --git a/docs/src/main/asciidoc/elasticsearch-dev-services.adoc b/docs/src/main/asciidoc/elasticsearch-dev-services.adoc index 773c7f4754c29..cb92564986f5d 100644 --- a/docs/src/main/asciidoc/elasticsearch-dev-services.adoc +++ b/docs/src/main/asciidoc/elasticsearch-dev-services.adoc @@ -4,8 +4,9 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Dev Services for Elasticsearch - include::_attributes.adoc[] +:categories: data +:summary: Start Elasticsearch automatically in dev and test modes If any Elasticsearch-related extension is present (e.g. `quarkus-elasticsearch-rest-client` or `quarkus-hibernate-search-orm-elasticsearch`), Dev Services for Elasticsearch automatically starts an Elasticsearch server in dev mode and when running tests. @@ -48,14 +49,20 @@ Note that the Elasticsearch hosts property is automatically configured with the == Configuring the image -Dev Services for Elasticsearch only support Elasticsearch based images, OpenSearch is not supported at the moment. +Dev Services for Elasticsearch support distributions based on both Elasticsearch and OpenSearch images. -If you need to use a different image than the default one you can configure it via: -[source, properties] +If you need to use a different Elasticsearch-based image than the default one you can configure it via: +[source,properties,subs="attributes"] ---- quarkus.elasticsearch.devservices.image-name={elasticsearch-image} ---- +Alternatively, if an OpenSearch-based image should be used, the same property can be used: +[source,properties,subs="attributes"] +---- +quarkus.elasticsearch.devservices.image-name={opensearch-image} +---- + == Current limitations Currently, only the default backend for Hibernate Search Elasticsearch is supported, because Dev Services for Elasticsearch can only start one Elasticsearch container. diff --git a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java index 1ffa51b9ab7b3..0049cc1348834 100644 --- a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java +++ b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java @@ -3,7 +3,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; -import java.util.Collections; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; @@ -43,7 +44,8 @@ public static String configureSharedNetwork(GenericContainer container, Strin } String hostName = (hostNamePrefix + "-" + Base58.randomString(5)).toLowerCase(Locale.ROOT); - container.setNetworkAliases(Collections.singletonList(hostName)); + // some containers might try to add their own aliases on start, so we want to keep this list modifiable: + container.setNetworkAliases(new ArrayList<>(List.of(hostName))); return hostName; } diff --git a/extensions/elasticsearch-rest-client-common/deployment/pom.xml b/extensions/elasticsearch-rest-client-common/deployment/pom.xml index 560553727270f..9439464ca2156 100644 --- a/extensions/elasticsearch-rest-client-common/deployment/pom.xml +++ b/extensions/elasticsearch-rest-client-common/deployment/pom.xml @@ -39,6 +39,10 @@ + + org.opensearch + opensearch-testcontainers + diff --git a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java index bb30ad5c22fb5..c1ab6fbe0b263 100644 --- a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java +++ b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java @@ -11,6 +11,8 @@ import java.util.function.Supplier; import org.jboss.logging.Logger; +import org.opensearch.testcontainers.OpensearchContainer; +import org.testcontainers.containers.GenericContainer; import org.testcontainers.elasticsearch.ElasticsearchContainer; import org.testcontainers.utility.DockerImageName; @@ -31,6 +33,7 @@ import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerAddress; import io.quarkus.devservices.common.ContainerLocator; +import io.quarkus.elasticsearch.restclient.common.deployment.ElasticsearchDevServicesBuildTimeConfig.Distribution; import io.quarkus.runtime.configuration.ConfigUtils; /** @@ -49,6 +52,9 @@ public class DevServicesElasticsearchProcessor { private static final ContainerLocator elasticsearchContainerLocator = new ContainerLocator(DEV_SERVICE_LABEL, ELASTICSEARCH_PORT); + private static final String DEFAULT_ELASTICSEARCH_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:8.8.2"; + private static final String DEFAULT_OPEN_SEARCH_IMAGE = "docker.io/opensearchproject/opensearch:2.9.0"; + private static final Distribution DEFAULT_DISTRIBUTION = Distribution.ELASTIC; static volatile DevServicesResultBuildItem.RunningDevService devService; static volatile ElasticsearchDevServicesBuildTimeConfig cfg; @@ -171,15 +177,12 @@ private DevServicesResultBuildItem.RunningDevService startElasticsearch( return null; } - // We only support ELASTIC container for now - if (buildItemConfig.distribution == DevservicesElasticsearchBuildItem.Distribution.OPENSEARCH) { - throw new BuildException("Dev Services for Elasticsearch doesn't support OpenSearch", Collections.emptyList()); - } - + Distribution resolvedDistribution = resolveDistribution(config, buildItemConfig); // Hibernate Search Elasticsearch have a version configuration property, we need to check that it is coherent // with the image we are about to launch if (buildItemConfig.version != null) { - String containerTag = config.imageName.substring(config.imageName.indexOf(':') + 1); + String imageName = resolveImageName(config, resolvedDistribution); + String containerTag = DockerImageName.parse(imageName).getVersionPart(); if (!containerTag.startsWith(buildItemConfig.version)) { throw new BuildException( "Dev Services for Elasticsearch detected a version mismatch, container image is " + config.imageName @@ -189,6 +192,15 @@ private DevServicesResultBuildItem.RunningDevService startElasticsearch( } } + if (buildItemConfig.distribution != null + && !config.distribution.orElse(buildItemConfig.distribution).equals(buildItemConfig.distribution)) { + throw new BuildException( + "Dev Services for Elasticsearch detected a distribution mismatch, distribution is " + config.distribution + + " but the configured distribution is " + buildItemConfig.distribution + + ". Either configure the same distribution or disable Dev Services for Elasticsearch.", + Collections.emptyList()); + } + final Optional maybeContainerAddress = elasticsearchContainerLocator.locateContainer( config.serviceName, config.shared, @@ -196,10 +208,11 @@ private DevServicesResultBuildItem.RunningDevService startElasticsearch( // Starting the server final Supplier defaultElasticsearchSupplier = () -> { - ElasticsearchContainer container = new ElasticsearchContainer( - DockerImageName.parse(config.imageName) - .asCompatibleSubstituteFor("docker.elastic.co/elasticsearch/elasticsearch")); - ConfigureUtil.configureSharedNetwork(container, "elasticsearch"); + + GenericContainer container = resolvedDistribution.equals(Distribution.ELASTIC) + ? createElasticsearchContainer(config) + : createOpensearchContainer(config); + if (config.serviceName != null) { container.withLabel(DEV_SERVICE_LABEL, config.serviceName); } @@ -208,22 +221,13 @@ private DevServicesResultBuildItem.RunningDevService startElasticsearch( } timeout.ifPresent(container::withStartupTimeout); - container.addEnv("ES_JAVA_OPTS", config.javaOpts); - // Disable security as else we would need to configure it correctly to avoid tons of WARNING in the log - container.addEnv("xpack.security.enabled", "false"); - // Disable disk-based shard allocation thresholds: - // in a single-node setup they just don't make sense, - // and lead to problems on large disks with little space left. - // See https://www.elastic.co/guide/en/elasticsearch/reference/8.8/modules-cluster.html#disk-based-shard-allocation - container.addEnv("cluster.routing.allocation.disk.threshold_enabled", "false"); - container.withEnv(config.containerEnv); container.start(); return new DevServicesResultBuildItem.RunningDevService(Feature.ELASTICSEARCH_REST_CLIENT_COMMON.getName(), container.getContainerId(), container::close, - buildPropertiesMap(buildItemConfig, container.getHttpHostAddress())); + buildPropertiesMap(buildItemConfig, container.getHost() + ":" + container.getMappedPort(9200))); }; return maybeContainerAddress @@ -235,6 +239,75 @@ private DevServicesResultBuildItem.RunningDevService startElasticsearch( .orElseGet(defaultElasticsearchSupplier); } + private GenericContainer createElasticsearchContainer(ElasticsearchDevServicesBuildTimeConfig config) { + ElasticsearchContainer container = new ElasticsearchContainer( + DockerImageName.parse(config.imageName.orElse(DEFAULT_ELASTICSEARCH_IMAGE)) + .asCompatibleSubstituteFor("docker.elastic.co/elasticsearch/elasticsearch")); + ConfigureUtil.configureSharedNetwork(container, "elasticsearch"); + + // Disable security as else we would need to configure it correctly to avoid tons of WARNING in the log + container.addEnv("xpack.security.enabled", "false"); + // Disable disk-based shard allocation thresholds: + // in a single-node setup they just don't make sense, + // and lead to problems on large disks with little space left. + // See https://www.elastic.co/guide/en/elasticsearch/reference/8.8/modules-cluster.html#disk-based-shard-allocation + container.addEnv("cluster.routing.allocation.disk.threshold_enabled", "false"); + container.addEnv("ES_JAVA_OPTS", config.javaOpts); + return container; + } + + private GenericContainer createOpensearchContainer(ElasticsearchDevServicesBuildTimeConfig config) { + OpensearchContainer container = new OpensearchContainer( + DockerImageName.parse(config.imageName.orElse(DevServicesElasticsearchProcessor.DEFAULT_OPEN_SEARCH_IMAGE)) + .asCompatibleSubstituteFor("opensearchproject/opensearch")); + container.addEnv("OPENSEARCH_JAVA_OPTS", config.javaOpts); + container.addEnv("bootstrap.memory_lock", "true"); + container.addEnv("plugins.index_state_management.enabled", "false"); + ConfigureUtil.configureSharedNetwork(container, "opensearch"); + return container; + } + + private String resolveImageName(ElasticsearchDevServicesBuildTimeConfig config, + Distribution resolvedDistribution) { + return config.imageName.orElseGet( + () -> Distribution.ELASTIC.equals(resolvedDistribution) + ? DEFAULT_ELASTICSEARCH_IMAGE + : DEFAULT_OPEN_SEARCH_IMAGE); + } + + private Distribution resolveDistribution(ElasticsearchDevServicesBuildTimeConfig config, + DevservicesElasticsearchBuildItemsConfiguration buildItemConfig) throws BuildException { + // If the build item has a distribution -- great: + if (buildItemConfig.distribution != null) { + return buildItemConfig.distribution; + } + // Otherwise, let's see if it was explicitly configured: + if (config.distribution.isPresent()) { + return config.distribution.get(); + } + // Now let's see if we can guess it from the image: + if (config.imageName.isPresent()) { + String imageNameRepository = DockerImageName.parse(config.imageName.get()).getRepository(); + if ("opensearchproject/opensearch".equalsIgnoreCase(imageNameRepository)) { + return Distribution.OPENSEARCH; + } + if ("elasticsearch/elasticsearch".equalsIgnoreCase(imageNameRepository) + || "elastic/elasticsearch".equalsIgnoreCase(imageNameRepository)) { + return Distribution.ELASTIC; + } + // no luck guessing so let's ask user to be more explicit: + throw new BuildException( + "Wasn't able to determine the distribution of the search service based on the provided image name [" + + config.imageName.get() + + "]. Please specify the distribution explicitly.", + Collections.emptyList()); + } + // If we didn't get an explicit distribution + // and no image name was provided + // then elastic is a default distribution: + return DEFAULT_DISTRIBUTION; + } + private Map buildPropertiesMap(DevservicesElasticsearchBuildItemsConfiguration buildItemConfig, String httpHosts) { Map propertiesToSet = new HashMap<>(); @@ -251,7 +324,7 @@ private String displayProperties(Set hostsConfigProperties) { private static class DevservicesElasticsearchBuildItemsConfiguration { private Set hostsConfigProperties; private String version; - private DevservicesElasticsearchBuildItem.Distribution distribution; + private Distribution distribution; private DevservicesElasticsearchBuildItemsConfiguration(List buildItems) throws BuildException { diff --git a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevservicesElasticsearchBuildItem.java b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevservicesElasticsearchBuildItem.java index b3063f14a0132..c241a53ff9d1f 100644 --- a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevservicesElasticsearchBuildItem.java +++ b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevservicesElasticsearchBuildItem.java @@ -1,5 +1,7 @@ package io.quarkus.elasticsearch.restclient.common.deployment; +import static io.quarkus.elasticsearch.restclient.common.deployment.ElasticsearchDevServicesBuildTimeConfig.Distribution; + import io.quarkus.builder.item.MultiBuildItem; public final class DevservicesElasticsearchBuildItem extends MultiBuildItem { @@ -11,7 +13,7 @@ public final class DevservicesElasticsearchBuildItem extends MultiBuildItem { public DevservicesElasticsearchBuildItem(String hostsConfigProperty) { this.hostsConfigProperty = hostsConfigProperty; this.version = null; - this.distribution = Distribution.ELASTIC; + this.distribution = null; } public DevservicesElasticsearchBuildItem(String configItemName, String version, Distribution distribution) { @@ -32,8 +34,4 @@ public Distribution getDistribution() { return distribution; } - public enum Distribution { - ELASTIC, - OPENSEARCH - } } diff --git a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchDevServicesBuildTimeConfig.java b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchDevServicesBuildTimeConfig.java index 8647c4a94afb8..1d6b0329222e0 100644 --- a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchDevServicesBuildTimeConfig.java +++ b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchDevServicesBuildTimeConfig.java @@ -27,12 +27,19 @@ public class ElasticsearchDevServicesBuildTimeConfig { @ConfigItem public Optional port; + /** + * The distribution of search services to use. + * Defaults to the Elasticsearch distribution. + */ + @ConfigItem + public Optional distribution; + /** * The Elasticsearch container image to use. * Defaults to the elasticsearch image provided by Elastic. */ - @ConfigItem(defaultValue = "docker.elastic.co/elasticsearch/elasticsearch:8.8.2") - public String imageName; + @ConfigItem + public Optional imageName; /** * The value for the ES_JAVA_OPTS env variable. @@ -84,6 +91,7 @@ public boolean equals(Object o) { return Objects.equals(shared, that.shared) && Objects.equals(enabled, that.enabled) && Objects.equals(port, that.port) + && Objects.equals(distribution, that.distribution) && Objects.equals(imageName, that.imageName) && Objects.equals(javaOpts, that.javaOpts) && Objects.equals(serviceName, that.serviceName) @@ -92,6 +100,11 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(enabled, port, imageName, javaOpts, shared, serviceName, containerEnv); + return Objects.hash(enabled, port, distribution, imageName, javaOpts, shared, serviceName, containerEnv); + } + + public enum Distribution { + ELASTIC, + OPENSEARCH } } diff --git a/extensions/hibernate-search-orm-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/deployment/HibernateSearchElasticsearchProcessor.java b/extensions/hibernate-search-orm-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/deployment/HibernateSearchElasticsearchProcessor.java index 33efa35f665f1..d89ae9184541a 100644 --- a/extensions/hibernate-search-orm-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/deployment/HibernateSearchElasticsearchProcessor.java +++ b/extensions/hibernate-search-orm-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/deployment/HibernateSearchElasticsearchProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.hibernate.search.orm.elasticsearch.deployment; +import static io.quarkus.elasticsearch.restclient.common.deployment.ElasticsearchDevServicesBuildTimeConfig.Distribution; import static io.quarkus.hibernate.search.orm.elasticsearch.deployment.ClassNames.INDEXED; import static io.quarkus.hibernate.search.orm.elasticsearch.deployment.ClassNames.PROJECTION_CONSTRUCTOR; import static io.quarkus.hibernate.search.orm.elasticsearch.deployment.ClassNames.ROOT_MAPPING; @@ -402,7 +403,7 @@ DevservicesElasticsearchBuildItem devServices(HibernateSearchElasticsearchBuildT "hosts"); return new DevservicesElasticsearchBuildItem(hostsPropertyKey, version.versionString(), - DevservicesElasticsearchBuildItem.Distribution.valueOf(version.distribution().toString().toUpperCase())); + Distribution.valueOf(version.distribution().toString().toUpperCase())); } @BuildStep(onlyIfNot = IsNormal.class) diff --git a/integration-tests/hibernate-search-orm-elasticsearch/README.md b/integration-tests/hibernate-search-orm-elasticsearch/README.md index 8569383f9017e..733493c55a4cc 100644 --- a/integration-tests/hibernate-search-orm-elasticsearch/README.md +++ b/integration-tests/hibernate-search-orm-elasticsearch/README.md @@ -7,12 +7,12 @@ By default, the tests of this module are disabled. To run the tests in a standard JVM with Elasticsearch started in the JVM, you can run the following command: ``` -mvn clean install -Dtest-containers +mvn clean install -Dtest-containers -Dstart-containers ``` Additionally, you can generate a native image and run the tests for this native image by adding `-Dnative`: ``` -mvn clean install -Dtest-containers -Dnative +mvn clean install -Dtest-containers -Dstart-containers -Dnative ``` diff --git a/integration-tests/hibernate-search-orm-opensearch/README.md b/integration-tests/hibernate-search-orm-opensearch/README.md index ad0835db80754..a4d9c62ddbaf3 100644 --- a/integration-tests/hibernate-search-orm-opensearch/README.md +++ b/integration-tests/hibernate-search-orm-opensearch/README.md @@ -7,12 +7,12 @@ By default, the tests of this module are disabled. To run the tests in a standard JVM with OpenSearch started in the JVM, you can run the following command: ``` -mvn clean install -Dtest-containers +mvn clean install -Dtest-containers -Dstart-containers ``` Additionally, you can generate a native image and run the tests for this native image by adding `-Dnative`: ``` -mvn clean install -Dtest-containers -Dnative +mvn clean install -Dtest-containers -Dstart-containers -Dnative ``` diff --git a/integration-tests/hibernate-search-orm-opensearch/src/main/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchDevServicesTestResource.java b/integration-tests/hibernate-search-orm-opensearch/src/main/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchDevServicesTestResource.java new file mode 100644 index 0000000000000..5a651d21ba19d --- /dev/null +++ b/integration-tests/hibernate-search-orm-opensearch/src/main/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchDevServicesTestResource.java @@ -0,0 +1,71 @@ +package io.quarkus.it.hibernate.search.orm.opensearch.devservices; + +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.search.mapper.orm.schema.management.SchemaManagementStrategyName; +import org.hibernate.search.mapper.orm.session.SearchSession; + +@Path("/test/dev-services") +public class HibernateSearchDevServicesTestResource { + + @Inject + SessionFactory sessionFactory; + + @Inject + Session session; + + @Inject + SearchSession searchSession; + + @GET + @Path("/hosts") + @Transactional + @SuppressWarnings("unchecked") + public String hosts() { + return ((List) sessionFactory.getProperties().get("hibernate.search.backend.hosts")).iterator().next(); + } + + @GET + @Path("/schema-management-strategy") + @Transactional + public String schemaManagementStrategy() { + var strategy = ((SchemaManagementStrategyName) sessionFactory.getProperties() + .get("hibernate.search.schema_management.strategy")); + return strategy == null ? null : strategy.externalRepresentation(); + } + + @PUT + @Path("/init-data") + @Transactional + public void initData() { + IndexedEntity entity = new IndexedEntity("John Irving"); + session.persist(entity); + } + + @PUT + @Path("/refresh") + @Produces(MediaType.TEXT_PLAIN) + public String refresh() { + searchSession.workspace().refresh(); + return "OK"; + } + + @GET + @Path("/count") + @Produces(MediaType.TEXT_PLAIN) + public long count() { + return searchSession.search(IndexedEntity.class) + .where(f -> f.matchAll()) + .fetchTotalHitCount(); + } +} diff --git a/integration-tests/hibernate-search-orm-opensearch/src/main/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/IndexedEntity.java b/integration-tests/hibernate-search-orm-opensearch/src/main/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/IndexedEntity.java new file mode 100644 index 0000000000000..fecdad1360bbf --- /dev/null +++ b/integration-tests/hibernate-search-orm-opensearch/src/main/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/IndexedEntity.java @@ -0,0 +1,47 @@ +package io.quarkus.it.hibernate.search.orm.opensearch.devservices; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +import org.hibernate.search.engine.backend.types.Sortable; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField; + +@Entity +@Indexed +public class IndexedEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "personSeq") + private Long id; + + @FullTextField(analyzer = "standard") + @KeywordField(name = "name_sort", normalizer = "lowercase", sortable = Sortable.YES) + private String name; + + public IndexedEntity() { + } + + public IndexedEntity(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/integration-tests/hibernate-search-orm-opensearch/src/main/resources/application.properties b/integration-tests/hibernate-search-orm-opensearch/src/main/resources/application.properties index 94c13ed99e2e1..f398b65412b8d 100644 --- a/integration-tests/hibernate-search-orm-opensearch/src/main/resources/application.properties +++ b/integration-tests/hibernate-search-orm-opensearch/src/main/resources/application.properties @@ -7,8 +7,14 @@ quarkus.datasource.jdbc.max-size=8 quarkus.hibernate-orm.database.generation=drop-and-create quarkus.hibernate-search-orm.elasticsearch.version=opensearch:1.2 -quarkus.hibernate-search-orm.elasticsearch.hosts=${opensearch.hosts:localhost:9200} -quarkus.hibernate-search-orm.elasticsearch.protocol=${opensearch.protocol:http} quarkus.hibernate-search-orm.elasticsearch.analysis.configurer=bean:backend-analysis quarkus.hibernate-search-orm.schema-management.strategy=drop-and-create-and-drop quarkus.hibernate-search-orm.indexing.plan.synchronization.strategy=sync + +# Use drop-and-create instead of drop-and-create-and-drop +# so we can differentiate between the value we set here +# and the value set automatically by the extension when using dev services +# See io.quarkus.it.hibernate.search.orm.opensearch.devservices.HibernateSearchElasticsearchDevServicesEnabledImplicitlyTest.testHibernateSearch +%test.quarkus.hibernate-search-orm.schema-management.strategy=drop-and-create +%test.quarkus.hibernate-search-orm.elasticsearch.hosts=${opensearch.hosts:localhost:9200} +%test.quarkus.hibernate-search-orm.elasticsearch.protocol=${opensearch.protocol:http} \ No newline at end of file diff --git a/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/DevServicesContextSpy.java b/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/DevServicesContextSpy.java new file mode 100644 index 0000000000000..c4d78dd73aaa4 --- /dev/null +++ b/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/DevServicesContextSpy.java @@ -0,0 +1,32 @@ +package io.quarkus.it.hibernate.search.orm.opensearch.devservices; + +import java.util.Collections; +import java.util.Map; + +import io.quarkus.test.common.DevServicesContext; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class DevServicesContextSpy implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware { + + DevServicesContext devServicesContext; + + @Override + public void setIntegrationTestContext(DevServicesContext context) { + this.devServicesContext = context; + } + + @Override + public Map start() { + return Collections.emptyMap(); + } + + @Override + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields(devServicesContext, f -> f.getType().isAssignableFrom(DevServicesContext.class)); + } + + @Override + public void stop() { + + } +} diff --git a/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesConfiguredExplicitlyTest.java b/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesConfiguredExplicitlyTest.java new file mode 100644 index 0000000000000..0c93ac2e51b02 --- /dev/null +++ b/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesConfiguredExplicitlyTest.java @@ -0,0 +1,85 @@ +package io.quarkus.it.hibernate.search.orm.opensearch.devservices; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.common.DevServicesContext; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import io.restassured.RestAssured; + +@QuarkusTest +@DisabledOnOs(OS.WINDOWS) +@TestProfile(HibernateSearchElasticsearchDevServicesConfiguredExplicitlyTest.Profile.class) +public class HibernateSearchElasticsearchDevServicesConfiguredExplicitlyTest { + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.elasticsearch.devservices.enabled", "true", + "quarkus.elasticsearch.devservices.image-name", "docker.io/opensearchproject/opensearch:2.6.0", + "quarkus.hibernate-search-orm.elasticsearch.version", "opensearch:2"); + } + + @Override + public String getConfigProfile() { + // Don't use %test properties; + // that way, we can control whether quarkus.hibernate-search-orm.elasticsearch.hosts is set or not. + // In this test, we do NOT set quarkus.hibernate-search-orm.elasticsearch.hosts. + return "someotherprofile"; + } + + @Override + public List testResources() { + // Enables injection of DevServicesContext + return List.of(new TestResourceEntry(DevServicesContextSpy.class)); + } + } + + DevServicesContext context; + + @Test + public void testDevServicesProperties() { + assertThat(context.devServicesProperties()) + .containsKey("quarkus.hibernate-search-orm.elasticsearch.hosts"); + assertThat(context.devServicesProperties().get("quarkus.hibernate-search-orm.elasticsearch.hosts")) + .isNotEmpty() + .isNotEqualTo("localhost:9200"); + } + + @Test + public void testHibernateSearch() { + RestAssured.when().get("/test/dev-services/hosts").then() + .statusCode(200) + .body(is(context.devServicesProperties().get("quarkus.hibernate-search-orm.elasticsearch.hosts"))); + + RestAssured.when().get("/test/dev-services/schema-management-strategy").then() + .statusCode(200) + // If the value is drop-and-create, this would indicate we're using the %test profile: + // that would be a bug in this test (see the Profile class above). + .body(is("drop-and-create-and-drop")); + + RestAssured.when().get("/test/dev-services/count").then() + .statusCode(200) + .body(is("0")); + + RestAssured.when().put("/test/dev-services/init-data").then() + .statusCode(204); + + RestAssured.when().put("/test/hibernate-search/refresh").then() + .statusCode(200) + .body(is("OK")); + + RestAssured.when().get("/test/dev-services/count").then() + .statusCode(200) + .body(is("1")); + } +} diff --git a/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesDisabledExplicitlyTest.java b/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesDisabledExplicitlyTest.java new file mode 100644 index 0000000000000..c6ad9da0a6173 --- /dev/null +++ b/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesDisabledExplicitlyTest.java @@ -0,0 +1,75 @@ +package io.quarkus.it.hibernate.search.orm.opensearch.devservices; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.common.DevServicesContext; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import io.restassured.RestAssured; + +@QuarkusTest +@DisabledOnOs(OS.WINDOWS) +@TestProfile(HibernateSearchElasticsearchDevServicesDisabledExplicitlyTest.Profile.class) +public class HibernateSearchElasticsearchDevServicesDisabledExplicitlyTest { + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + Map config = new HashMap<>(); // Cannot use Map.of, we need nulls + // Even if quarkus.hibernate-search-orm.elasticsearch.hosts is not set, + // Quarkus won't start Elasticsearch dev-services because of this explicit setting: + config.put("quarkus.elasticsearch.devservices.enabled", "false"); + // Ensure we can work offline, because without dev-services, + // we won't have an Elasticsearch instance to talk to. + config.putAll(Map.of( + "quarkus.hibernate-search-orm.schema-management.strategy", "none", + // This version does not matter as long as it's supported by Hibernate Search: + // it won't be checked in this test anyway. + "quarkus.hibernate-search-orm.elasticsearch.version", "opensearch:2.9.0", + "quarkus.hibernate-search-orm.elasticsearch.version-check.enabled", "false")); + return config; + } + + @Override + public String getConfigProfile() { + // Don't use %test properties; + // that way, we can control whether quarkus.hibernate-search-orm.elasticsearch.hosts is set or not. + // In this test, we do NOT set quarkus.hibernate-search-orm.elasticsearch.hosts. + return "someotherprofile"; + } + + @Override + public List testResources() { + // Enables injection of DevServicesContext + return List.of(new TestResourceEntry(DevServicesContextSpy.class)); + } + } + + DevServicesContext context; + + @Test + public void testDevServicesProperties() { + assertThat(context.devServicesProperties()) + .doesNotContainKey("quarkus.hibernate-search-orm.elasticsearch.hosts"); + } + + @Test + public void testHibernateSearch() { + RestAssured.when().get("/test/dev-services/hosts").then() + .statusCode(200) + .body(is("localhost:9200")); // This is the default + + // We don't test Hibernate Search features (indexing, search) here, + // because we're not sure that there is a host that Hibernate Search can talk to. + // It's fine, though: we checked that Hibernate Search is configured as intended. + } +} diff --git a/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesDisabledImplicitlyTest.java b/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesDisabledImplicitlyTest.java new file mode 100644 index 0000000000000..53b66df2031fa --- /dev/null +++ b/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesDisabledImplicitlyTest.java @@ -0,0 +1,73 @@ +package io.quarkus.it.hibernate.search.orm.opensearch.devservices; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.common.DevServicesContext; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import io.restassured.RestAssured; + +@QuarkusTest +@DisabledOnOs(OS.WINDOWS) +@TestProfile(HibernateSearchElasticsearchDevServicesDisabledImplicitlyTest.Profile.class) +public class HibernateSearchElasticsearchDevServicesDisabledImplicitlyTest { + private static final String EXPLICIT_HOSTS = "mycompany.com:4242"; + + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + // Make sure quarkus.hibernate-search-orm.elasticsearch.hosts is set, + // so that Quarkus detects disables Elasticsearch dev-services implicitly. + "quarkus.hibernate-search-orm.elasticsearch.hosts", EXPLICIT_HOSTS, + // Ensure we can work offline, because the host we set just above does not actually exist. + "quarkus.hibernate-search-orm.schema-management.strategy", "none", + // This version does not matter as long as it's supported by Hibernate Search: + // it won't be checked in this test anyway. + "quarkus.hibernate-search-orm.elasticsearch.version", "opensearch:2.9.0", + "quarkus.hibernate-search-orm.elasticsearch.version-check.enabled", "false"); + } + + @Override + public String getConfigProfile() { + // Don't use %test properties; + // that way, we can control whether quarkus.hibernate-search-orm.elasticsearch.hosts is set or not. + // In this test, we DO set quarkus.hibernate-search-orm.elasticsearch.hosts (see above). + return "someotherprofile"; + } + + @Override + public List testResources() { + // Enables injection of DevServicesContext + return List.of(new TestResourceEntry(DevServicesContextSpy.class)); + } + } + + DevServicesContext context; + + @Test + public void testDevServicesProperties() { + assertThat(context.devServicesProperties()) + .doesNotContainKey("quarkus.hibernate-search-orm.elasticsearch.hosts"); + } + + @Test + public void testHibernateSearch() { + RestAssured.when().get("/test/dev-services/hosts").then() + .statusCode(200) + .body(is(EXPLICIT_HOSTS)); + + // We don't test Hibernate Search features (indexing, search) here, + // because we're not sure that there is a host that Hibernate Search can talk to. + // It's fine, though: we checked that Hibernate Search is configured as intended. + } +} diff --git a/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesEnabledImplicitlyTest.java b/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesEnabledImplicitlyTest.java new file mode 100644 index 0000000000000..b7f6462c93a60 --- /dev/null +++ b/integration-tests/hibernate-search-orm-opensearch/src/test/java/io/quarkus/it/hibernate/search/orm/opensearch/devservices/HibernateSearchElasticsearchDevServicesEnabledImplicitlyTest.java @@ -0,0 +1,83 @@ +package io.quarkus.it.hibernate.search.orm.opensearch.devservices; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.common.DevServicesContext; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import io.restassured.RestAssured; + +@QuarkusTest +@DisabledOnOs(OS.WINDOWS) +@TestProfile(HibernateSearchElasticsearchDevServicesEnabledImplicitlyTest.Profile.class) +public class HibernateSearchElasticsearchDevServicesEnabledImplicitlyTest { + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.hibernate-search-orm.elasticsearch.version", "opensearch:2.9.0"); + } + + @Override + public String getConfigProfile() { + // Don't use %test properties; + // that way, we can control whether quarkus.hibernate-search-orm.elasticsearch.hosts is set or not. + // In this test, we do NOT set quarkus.hibernate-search-orm.elasticsearch.hosts. + return "someotherprofile"; + } + + @Override + public List testResources() { + // Enables injection of DevServicesContext + return List.of(new TestResourceEntry(DevServicesContextSpy.class)); + } + } + + DevServicesContext context; + + @Test + public void testDevServicesProperties() { + assertThat(context.devServicesProperties()) + .containsKey("quarkus.hibernate-search-orm.elasticsearch.hosts"); + assertThat(context.devServicesProperties().get("quarkus.hibernate-search-orm.elasticsearch.hosts")) + .isNotEmpty() + .isNotEqualTo("localhost:9200"); + } + + @Test + public void testHibernateSearch() { + RestAssured.when().get("/test/dev-services/hosts").then() + .statusCode(200) + .body(is(context.devServicesProperties().get("quarkus.hibernate-search-orm.elasticsearch.hosts"))); + + RestAssured.when().get("/test/dev-services/schema-management-strategy").then() + .statusCode(200) + // If the value is drop-and-create, this would indicate we're using the %test profile: + // that would be a bug in this test (see the Profile class above). + .body(is("drop-and-create-and-drop")); + + RestAssured.when().get("/test/dev-services/count").then() + .statusCode(200) + .body(is("0")); + + RestAssured.when().put("/test/dev-services/init-data").then() + .statusCode(204); + + RestAssured.when().put("/test/hibernate-search/refresh").then() + .statusCode(200) + .body(is("OK")); + + RestAssured.when().get("/test/dev-services/count").then() + .statusCode(200) + .body(is("1")); + } +}