From ddce01770c145ffaac0004c99592420d4db1284b Mon Sep 17 00:00:00 2001 From: Sven Boeckelmann Date: Thu, 22 Dec 2022 18:41:57 +0100 Subject: [PATCH] initial commit of copy from quarkus-elasticsearch extension --- .gitignore | 65 +++++ README.md | 3 + .../deployment/pom.xml | 60 ++++ .../DevServicesOpenSearchProcessor.java | 274 ++++++++++++++++++ .../DevservicesOpenSearchBuildItem.java | 39 +++ .../OpenSearchDevServicesBuildTimeConfig.java | 68 +++++ .../OpenSearchRestClientProcessor.java | 23 ++ opensearch-rest-client-common/pom.xml | 20 ++ opensearch-rest-client-common/runtime/pom.xml | 75 +++++ .../runtime/graal/Substitute_RestClient.java | 131 +++++++++ .../resources/META-INF/quarkus-extension.yaml | 10 + opensearch-rest-client/deployment/pom.xml | 101 +++++++ .../deployment/OpenSearchBuildTimeConfig.java | 14 + .../OpenSearchLowLevelClientProcessor.java | 52 ++++ .../DevServicesOpenSearchDevModeTestCase.java | 34 +++ .../runtime/OpenSearchClientConfigTest.java | 49 ++++ .../lowlevel/runtime/TestResource.java | 65 +++++ opensearch-rest-client/pom.xml | 22 ++ opensearch-rest-client/runtime/pom.xml | 58 ++++ .../lowlevel/OpenSearchClientConfig.java | 28 ++ .../lowlevel/runtime/OpenSearchConfig.java | 95 ++++++ .../runtime/OpenSearchRestClientProducer.java | 52 ++++ .../runtime/RestClientBuilderHelper.java | 115 ++++++++ .../runtime/health/OpenSearchHealthCheck.java | 43 +++ .../resources/META-INF/quarkus-extension.yaml | 13 + .../deployment/pom.xml | 58 ++++ .../OpenSearchHighLevelClientProcessor.java | 21 ++ opensearch-rest-high-level-client/pom.xml | 23 ++ .../runtime/pom.xml | 121 ++++++++ ...OpenSearchRestHighLevelClientProducer.java | 43 +++ .../runtime/QuarkusRestHighLevelClient.java | 22 ++ .../resources/META-INF/quarkus-extension.yaml | 12 + pom.xml | 133 +++++++++ 33 files changed, 1942 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 opensearch-rest-client-common/deployment/pom.xml create mode 100644 opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/DevServicesOpenSearchProcessor.java create mode 100644 opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/DevservicesOpenSearchBuildItem.java create mode 100644 opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/OpenSearchDevServicesBuildTimeConfig.java create mode 100644 opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/OpenSearchRestClientProcessor.java create mode 100644 opensearch-rest-client-common/pom.xml create mode 100644 opensearch-rest-client-common/runtime/pom.xml create mode 100644 opensearch-rest-client-common/runtime/src/main/java/io/quarkiverse/opensearch/restclient/common/runtime/graal/Substitute_RestClient.java create mode 100644 opensearch-rest-client-common/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 opensearch-rest-client/deployment/pom.xml create mode 100644 opensearch-rest-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/deployment/OpenSearchBuildTimeConfig.java create mode 100644 opensearch-rest-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/deployment/OpenSearchLowLevelClientProcessor.java create mode 100644 opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/DevServicesOpenSearchDevModeTestCase.java create mode 100644 opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchClientConfigTest.java create mode 100644 opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/TestResource.java create mode 100644 opensearch-rest-client/pom.xml create mode 100644 opensearch-rest-client/runtime/pom.xml create mode 100644 opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/OpenSearchClientConfig.java create mode 100644 opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchConfig.java create mode 100644 opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchRestClientProducer.java create mode 100644 opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/RestClientBuilderHelper.java create mode 100644 opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/health/OpenSearchHealthCheck.java create mode 100644 opensearch-rest-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 opensearch-rest-high-level-client/deployment/pom.xml create mode 100644 opensearch-rest-high-level-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/deployment/OpenSearchHighLevelClientProcessor.java create mode 100644 opensearch-rest-high-level-client/pom.xml create mode 100644 opensearch-rest-high-level-client/runtime/pom.xml create mode 100644 opensearch-rest-high-level-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/runtime/OpenSearchRestHighLevelClientProducer.java create mode 100644 opensearch-rest-high-level-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/runtime/QuarkusRestHighLevelClient.java create mode 100644 opensearch-rest-high-level-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6af45a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Gradle +.gradle/ +build/ + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.cache/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e48e5ec --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# quarkus-opensearch + +non-functional copy of quarkus-elasticsearch extension with replacements for Elasticsearch with OpenSearch \ No newline at end of file diff --git a/opensearch-rest-client-common/deployment/pom.xml b/opensearch-rest-client-common/deployment/pom.xml new file mode 100644 index 0000000..cbfa5fb --- /dev/null +++ b/opensearch-rest-client-common/deployment/pom.xml @@ -0,0 +1,60 @@ + + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-common-parent + 0.1.0-SNAPSHOT + + 4.0.0 + + quarkus-opensearch-rest-client-common-deployment + Quarkus - OpenSearch REST client common - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-apache-httpclient-deployment + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-common + + + io.quarkus + quarkus-devservices-deployment + + + org.opensearch + opensearch-testcontainers + + + junit + junit + + + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/DevServicesOpenSearchProcessor.java b/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/DevServicesOpenSearchProcessor.java new file mode 100644 index 0000000..4322f32 --- /dev/null +++ b/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/DevServicesOpenSearchProcessor.java @@ -0,0 +1,274 @@ +package io.quarkiverse.opensearch.restclient.common.deployment; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import org.jboss.logging.Logger; +import org.opensearch.testcontainers.OpensearchContainer; +import org.testcontainers.utility.DockerImageName; + +import io.quarkus.builder.BuildException; +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; +import io.quarkus.deployment.builditem.DevServicesResultBuildItem; +import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; +import io.quarkus.devservices.common.ConfigureUtil; +import io.quarkus.devservices.common.ContainerAddress; +import io.quarkus.devservices.common.ContainerLocator; +import io.quarkus.runtime.configuration.ConfigUtils; + +/** + * Starts an OpenSearch server as dev service if needed. + */ +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) +public class DevServicesOpenSearchProcessor { + private static final Logger log = Logger.getLogger(DevServicesOpenSearchProcessor.class); + + /** + * Label to add to shared Dev Service for OpenSearch running in containers. + * This allows other applications to discover the running service and use it instead of starting a new instance. + */ + static final String DEV_SERVICE_LABEL = "quarkus-dev-service-opensearch"; + static final int OPENSEARCH_PORT = 9200; + + private static final ContainerLocator openSearchContainerLocator = new ContainerLocator(DEV_SERVICE_LABEL, + OPENSEARCH_PORT); + + static volatile DevServicesResultBuildItem.RunningDevService devService; + static volatile OpenSearchDevServicesBuildTimeConfig cfg; + static volatile boolean first = true; + + @BuildStep + public DevServicesResultBuildItem startOpenSearchDevService( + DockerStatusBuildItem dockerStatusBuildItem, + LaunchModeBuildItem launchMode, + OpenSearchDevServicesBuildTimeConfig configuration, + List devServicesSharedNetworkBuildItem, + Optional consoleInstalledBuildItem, + CuratedApplicationShutdownBuildItem closeBuildItem, + LoggingSetupBuildItem loggingSetupBuildItem, + GlobalDevServicesConfig devServicesConfig, + List devservicesOpenSearchBuildItems) throws BuildException { + + if (devservicesOpenSearchBuildItems.isEmpty()) { + // safety belt in case a module depends on this one without producing the build item + return null; + } + + DevservicesOpenSearchBuildItemsConfiguration buildItemsConfig = new DevservicesOpenSearchBuildItemsConfiguration( + devservicesOpenSearchBuildItems); + + if (devService != null) { + boolean shouldShutdownTheServer = !configuration.equals(cfg); + if (!shouldShutdownTheServer) { + return devService.toBuildItem(); + } + shutdownOpenSearch(); + cfg = null; + } + + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + "OpenSearch Dev Services Starting:", + consoleInstalledBuildItem, loggingSetupBuildItem); + try { + devService = startOpenSearch(dockerStatusBuildItem, configuration, buildItemsConfig, launchMode, + !devServicesSharedNetworkBuildItem.isEmpty(), + devServicesConfig.timeout); + if (devService == null) { + compressor.closeAndDumpCaptured(); + } else { + compressor.close(); + } + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); + } + + if (devService == null) { + return null; + } + + // Configure the watch dog + if (first) { + first = false; + Runnable closeTask = () -> { + if (devService != null) { + shutdownOpenSearch(); + } + first = true; + devService = null; + cfg = null; + }; + closeBuildItem.addCloseTask(closeTask, true); + } + cfg = configuration; + + if (devService.isOwner()) { + log.infof( + "Dev Services for OpenSearch started. Other Quarkus applications in dev mode will find the " + + "server automatically. For Quarkus applications in production mode, you can connect to" + + " this by configuring your application to use %s", + getOpenSearchHosts(buildItemsConfig)); + } + return devService.toBuildItem(); + } + + public static String getOpenSearchHosts(DevservicesOpenSearchBuildItemsConfiguration buildItemsConfiguration) { + String hostsConfigProperty = buildItemsConfiguration.hostsConfigProperties.stream().findAny().get(); + return devService.getConfig().get(hostsConfigProperty); + } + + private void shutdownOpenSearch() { + if (devService != null) { + try { + devService.close(); + } catch (Throwable e) { + log.error("Failed to stop the OpenSearch server", e); + } finally { + devService = null; + } + } + } + + private DevServicesResultBuildItem.RunningDevService startOpenSearch( + DockerStatusBuildItem dockerStatusBuildItem, + OpenSearchDevServicesBuildTimeConfig config, + DevservicesOpenSearchBuildItemsConfiguration buildItemConfig, + LaunchModeBuildItem launchMode, boolean useSharedNetwork, Optional timeout) throws BuildException { + if (!config.enabled.orElse(true)) { + // explicitly disabled + log.debug("Not starting dev services for OpenSearch, as it has been disabled in the config."); + return null; + } + + for (String hostsConfigProperty : buildItemConfig.hostsConfigProperties) { + // Check if opensearch hosts property is set + if (ConfigUtils.isPropertyPresent(hostsConfigProperty)) { + log.debugf("Not starting dev services for OpenSearch, the %s property is configured.", hostsConfigProperty); + return null; + } + } + + if (!dockerStatusBuildItem.isDockerAvailable()) { + log.warnf("Docker isn't working, please configure the OpenSearch hosts property (%s).", + displayProperties(buildItemConfig.hostsConfigProperties)); + return null; + } + + // We only support OPENSEARCH container + if (buildItemConfig.distribution == DevservicesOpenSearchBuildItem.Distribution.ELASTIC) { + throw new BuildException("Dev services for OpenSearch doesn't support Elasticsearch", Collections.emptyList()); + } + + // Hibernate Search OpenSearch 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); + if (!containerTag.startsWith(buildItemConfig.version)) { + throw new BuildException( + "Dev services for OpenSearch detected a version mismatch, container image is " + config.imageName + + " but the configured version is " + buildItemConfig.version + + ". Either configure a different image or disable dev services for OpenSearch.", + Collections.emptyList()); + } + } + + final Optional maybeContainerAddress = openSearchContainerLocator.locateContainer( + config.serviceName, + config.shared, + launchMode.getLaunchMode()); + + // Starting the server + final Supplier defaultOpenSearchSupplier = () -> { + OpensearchContainer container = new OpensearchContainer( + DockerImageName.parse(config.imageName).asCompatibleSubstituteFor("opensearchproject/opensearch")); + ConfigureUtil.configureSharedNetwork(container, "opensearch"); + if (config.serviceName != null) { + container.withLabel(DEV_SERVICE_LABEL, config.serviceName); + } + if (config.port.isPresent()) { + container.setPortBindings(List.of(config.port.get() + ":" + config.port.get())); + } + 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("plugins.security.disabled", "false"); + + container.start(); + return new DevServicesResultBuildItem.RunningDevService(Feature.ELASTICSEARCH_REST_CLIENT_COMMON.getName(), + container.getContainerId(), + container::close, + buildPropertiesMap(buildItemConfig, container.getHttpHostAddress())); + }; + + return maybeContainerAddress + .map(containerAddress -> new DevServicesResultBuildItem.RunningDevService( + Feature.ELASTICSEARCH_REST_CLIENT_COMMON.getName(), + containerAddress.getId(), + null, + buildPropertiesMap(buildItemConfig, containerAddress.getUrl()))) + .orElseGet(defaultOpenSearchSupplier); + } + + private Map buildPropertiesMap(DevservicesOpenSearchBuildItemsConfiguration buildItemConfig, + String httpHosts) { + Map propertiesToSet = new HashMap<>(); + for (String property : buildItemConfig.hostsConfigProperties) { + propertiesToSet.put(property, httpHosts); + } + return propertiesToSet; + } + + private String displayProperties(Set hostsConfigProperties) { + return String.join(" and ", hostsConfigProperties); + } + + private static class DevservicesOpenSearchBuildItemsConfiguration { + private Set hostsConfigProperties; + private String version; + private DevservicesOpenSearchBuildItem.Distribution distribution; + + private DevservicesOpenSearchBuildItemsConfiguration(List buildItems) + throws BuildException { + hostsConfigProperties = new HashSet<>(buildItems.size()); + + // check that all build items agree on the version and distribution to start + for (DevservicesOpenSearchBuildItem buildItem : buildItems) { + if (version == null) { + version = buildItem.getVersion(); + } else if (!version.equals(buildItem.getVersion())) { + // safety guard but should never occur as only Hibernate Search ORM Elasticsearch configure the version + throw new BuildException("Multiple extensions request OpenSearch Dev Services on different version.", + Collections.emptyList()); + } + + if (distribution == null) { + distribution = buildItem.getDistribution(); + } else if (!distribution.equals(buildItem.getDistribution())) { + // safety guard but should never occur as only Hibernate Search ORM Elasticsearch configure the distribution + throw new BuildException( + "Multiple extensions request OpenSearch Dev Services on different distribution.", + Collections.emptyList()); + } + + hostsConfigProperties.add(buildItem.getHostsConfigProperty()); + } + } + } +} diff --git a/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/DevservicesOpenSearchBuildItem.java b/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/DevservicesOpenSearchBuildItem.java new file mode 100644 index 0000000..75af7b0 --- /dev/null +++ b/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/DevservicesOpenSearchBuildItem.java @@ -0,0 +1,39 @@ +package io.quarkiverse.opensearch.restclient.common.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class DevservicesOpenSearchBuildItem extends MultiBuildItem { + private final String hostsConfigProperty; + + private final String version; + private final Distribution distribution; + + public DevservicesOpenSearchBuildItem(String hostsConfigProperty) { + this.hostsConfigProperty = hostsConfigProperty; + this.version = null; + this.distribution = Distribution.OPENSEARCH; + } + + public DevservicesOpenSearchBuildItem(String configItemName, String version, Distribution distribution) { + this.hostsConfigProperty = configItemName; + this.version = version; + this.distribution = distribution; + } + + public String getHostsConfigProperty() { + return hostsConfigProperty; + } + + public String getVersion() { + return version; + } + + public Distribution getDistribution() { + return distribution; + } + + public enum Distribution { + ELASTIC, + OPENSEARCH + } +} diff --git a/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/OpenSearchDevServicesBuildTimeConfig.java b/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/OpenSearchDevServicesBuildTimeConfig.java new file mode 100644 index 0000000..db01517 --- /dev/null +++ b/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/OpenSearchDevServicesBuildTimeConfig.java @@ -0,0 +1,68 @@ +package io.quarkiverse.opensearch.restclient.common.deployment; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "opensearch.devservices", phase = ConfigPhase.BUILD_TIME) +public class OpenSearchDevServicesBuildTimeConfig { + + /** + * If Dev Services for OpenSearch has been explicitly enabled or disabled. Dev Services are generally enabled + * by default, unless there is an existing configuration present. For OpenSearch, Dev Services starts a server unless + * {@code quarkiverse.opensearch.hosts} is set. + */ + @ConfigItem + public Optional enabled = Optional.empty(); + + /** + * Optional fixed port the dev service will listen to. + *

+ * If not defined, the port will be chosen randomly. + */ + @ConfigItem + public Optional port; + + /** + * The OpenSearch container image to use. + * Defaults to the opensearch image provided by OpenSearch. + */ + @ConfigItem(defaultValue = "docker.io/opensearchproject/opensearch:2.4.1") + public String imageName; + + /** + * The value for the ES_JAVA_OPTS env variable. + * Defaults to setting the heap to 512MB min - 1GB max. + */ + @ConfigItem(defaultValue = "-Xms512m -Xmx1g") + public String javaOpts; + + /** + * Indicates if the OpenSearch server managed by Quarkus Dev Services is shared. + * When shared, Quarkus looks for running containers using label-based service discovery. + * If a matching container is found, it is used, and so a second one is not started. + * Otherwise, Dev Services for OpenSearch starts a new container. + *

+ * The discovery uses the {@code quarkus-dev-service-opensearch} label. + * The value is configured using the {@code service-name} property. + *

+ * Container sharing is only used in dev mode. + */ + @ConfigItem(defaultValue = "true") + public boolean shared; + + /** + * The value of the {@code quarkus-dev-service-opensearch} label attached to the started container. + * This property is used when {@code shared} is set to {@code true}. + * In this case, before starting a container, Dev Services for OpenSearch looks for a container with the + * {@code quarkus-dev-service-opensearch} label + * set to the configured value. If found, it will use this container instead of starting a new one. Otherwise it + * starts a new container with the {@code quarkus-dev-service-opensearch} label set to the specified value. + *

+ * This property is used when you need multiple shared OpenSearch servers. + */ + @ConfigItem(defaultValue = "opensearch") + public String serviceName; +} diff --git a/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/OpenSearchRestClientProcessor.java b/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/OpenSearchRestClientProcessor.java new file mode 100644 index 0000000..fc53ce6 --- /dev/null +++ b/opensearch-rest-client-common/deployment/src/main/java/io/quarkiverse/opensearch/restclient/common/deployment/OpenSearchRestClientProcessor.java @@ -0,0 +1,23 @@ +package io.quarkiverse.opensearch.restclient.common.deployment; + +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; + +class OpenSearchRestClientProcessor { + + @BuildStep + public void build(BuildProducer extensionSslNativeSupport) { + // Indicates that this extension would like the SSL support to be enabled + extensionSslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(Feature.ELASTICSEARCH_REST_CLIENT_COMMON)); + } + + @BuildStep + public ReflectiveClassBuildItem registerForReflection() { + return new ReflectiveClassBuildItem(true, true, + "org.apache.logging.log4j.message.ReusableMessageFactory", + "org.apache.logging.log4j.message.DefaultFlowMessageFactory"); + } +} diff --git a/opensearch-rest-client-common/pom.xml b/opensearch-rest-client-common/pom.xml new file mode 100644 index 0000000..aea44bc --- /dev/null +++ b/opensearch-rest-client-common/pom.xml @@ -0,0 +1,20 @@ + + + + io.quarkiverse.opensearch + quarkus-opensearch-parent + 0.1.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-opensearch-rest-client-common-parent + Quarkus - OpenSearch client common + pom + + deployment + runtime + + diff --git a/opensearch-rest-client-common/runtime/pom.xml b/opensearch-rest-client-common/runtime/pom.xml new file mode 100644 index 0000000..e05d32d --- /dev/null +++ b/opensearch-rest-client-common/runtime/pom.xml @@ -0,0 +1,75 @@ + + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-common-parent + 0.1.0-SNAPSHOT + + 4.0.0 + + quarkus-opensearch-rest-client-common + Quarkus - OpenSearch REST client common - Runtime + OpenSearch REST client common - Runtime + + + io.quarkus + quarkus-core + + + org.opensearch.client + opensearch-rest-client + + + commons-logging + commons-logging + + + + + io.quarkus + quarkus-apache-httpclient + + + org.jboss.logging + commons-logging-jboss-logging + + + org.opensearch.client + opensearch-rest-client-sniffer + + + commons-logging + commons-logging + + + + + org.graalvm.nativeimage + svm + provided + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/opensearch-rest-client-common/runtime/src/main/java/io/quarkiverse/opensearch/restclient/common/runtime/graal/Substitute_RestClient.java b/opensearch-rest-client-common/runtime/src/main/java/io/quarkiverse/opensearch/restclient/common/runtime/graal/Substitute_RestClient.java new file mode 100644 index 0000000..9f176c3 --- /dev/null +++ b/opensearch-rest-client-common/runtime/src/main/java/io/quarkiverse/opensearch/restclient/common/runtime/graal/Substitute_RestClient.java @@ -0,0 +1,131 @@ +package io.quarkiverse.opensearch.restclient.common.runtime.graal; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.http.HttpHost; +import org.apache.http.annotation.Contract; +import org.apache.http.annotation.ThreadingBehavior; +import org.apache.http.auth.AuthScheme; +import org.apache.http.client.AuthCache; +import org.apache.http.conn.SchemePortResolver; +import org.apache.http.conn.UnsupportedSchemeException; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.conn.DefaultSchemePortResolver; +import org.apache.http.util.Args; +import org.opensearch.client.Node; +import org.opensearch.client.RestClient; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +/** + * {@link BasicAuthCache} used in the {@link RestClient} is using + * serialization which is not supported by GraalVM. + *

+ * We substitute it with an implementation which does not use serialization. + */ +@TargetClass(className = "org.opensearch.client.RestClient") +final class Substitute_RestClient { + + @Alias + private ConcurrentMap blacklist; + + @Alias + private volatile NodeTuple> nodeTuple; + + @Substitute + public synchronized void setNodes(Collection nodes) { + if (nodes == null || nodes.isEmpty()) { + throw new IllegalArgumentException("nodes must not be null or empty"); + } + AuthCache authCache = new NoSerializationBasicAuthCache(); + + Map nodesByHost = new LinkedHashMap<>(); + for (Node node : nodes) { + Objects.requireNonNull(node, "node cannot be null"); + // TODO should we throw an IAE if we have two nodes with the same host? + nodesByHost.put(node.getHost(), node); + authCache.put(node.getHost(), new BasicScheme()); + } + this.nodeTuple = new NodeTuple<>(List.copyOf(nodesByHost.values()), authCache); + this.blacklist.clear(); + } + + @TargetClass(className = "org.opensearch.client.DeadHostState") + final static class DeadHostState { + } + + @TargetClass(className = "org.opensearch.client.RestClient", innerClass = "NodeTuple") + final static class NodeTuple { + + @Alias + NodeTuple(final T nodes, final AuthCache authCache) { + } + } + + @Contract(threading = ThreadingBehavior.SAFE) + private static final class NoSerializationBasicAuthCache implements AuthCache { + + private final Map map; + private final SchemePortResolver schemePortResolver; + + public NoSerializationBasicAuthCache(final SchemePortResolver schemePortResolver) { + this.map = new ConcurrentHashMap<>(); + this.schemePortResolver = schemePortResolver != null ? schemePortResolver + : DefaultSchemePortResolver.INSTANCE; + } + + public NoSerializationBasicAuthCache() { + this(null); + } + + protected HttpHost getKey(final HttpHost host) { + if (host.getPort() <= 0) { + final int port; + try { + port = schemePortResolver.resolve(host); + } catch (final UnsupportedSchemeException ignore) { + return host; + } + return new HttpHost(host.getHostName(), port, host.getSchemeName()); + } else { + return host; + } + } + + @Override + public void put(final HttpHost host, final AuthScheme authScheme) { + Args.notNull(host, "HTTP host"); + if (authScheme == null) { + return; + } + this.map.put(getKey(host), authScheme); + } + + @Override + public AuthScheme get(final HttpHost host) { + Args.notNull(host, "HTTP host"); + return this.map.get(getKey(host)); + } + + @Override + public void remove(final HttpHost host) { + Args.notNull(host, "HTTP host"); + this.map.remove(getKey(host)); + } + + @Override + public void clear() { + this.map.clear(); + } + + @Override + public String toString() { + return this.map.toString(); + } + } +} diff --git a/opensearch-rest-client-common/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/opensearch-rest-client-common/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000..d573cac --- /dev/null +++ b/opensearch-rest-client-common/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,10 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "OpenSearch REST Client Common" +metadata: + keywords: + - "search" + categories: + - "data" + status: "stable" + unlisted: "true" \ No newline at end of file diff --git a/opensearch-rest-client/deployment/pom.xml b/opensearch-rest-client/deployment/pom.xml new file mode 100644 index 0000000..8430163 --- /dev/null +++ b/opensearch-rest-client/deployment/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-parent + 0.1.0-SNAPSHOT + ../pom.xml + + + quarkus-opensearch-rest-client-deployment + Quarkus - OpenSearch REST client - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-common-deployment + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-smallrye-health-spi + + + + + io.quarkus + quarkus-junit5-internal + test + + + io.quarkus + quarkus-resteasy-jackson-deployment + test + + + io.rest-assured + rest-assured + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + maven-surefire-plugin + + true + + + + + + + + test-opensearch + + + test-containers + + + + + + maven-surefire-plugin + + false + + + + + + + + diff --git a/opensearch-rest-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/deployment/OpenSearchBuildTimeConfig.java b/opensearch-rest-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/deployment/OpenSearchBuildTimeConfig.java new file mode 100644 index 0000000..c70fd9e --- /dev/null +++ b/opensearch-rest-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/deployment/OpenSearchBuildTimeConfig.java @@ -0,0 +1,14 @@ +package io.quarkiverse.opensearch.restclient.lowlevel.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "opensearch", phase = ConfigPhase.BUILD_TIME) +public class OpenSearchBuildTimeConfig { + /** + * Whether a health check is published in case the smallrye-health extension is present. + */ + @ConfigItem(name = "health.enabled", defaultValue = "true") + public boolean healthEnabled; +} diff --git a/opensearch-rest-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/deployment/OpenSearchLowLevelClientProcessor.java b/opensearch-rest-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/deployment/OpenSearchLowLevelClientProcessor.java new file mode 100644 index 0000000..db94f58 --- /dev/null +++ b/opensearch-rest-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/deployment/OpenSearchLowLevelClientProcessor.java @@ -0,0 +1,52 @@ +package io.quarkiverse.opensearch.restclient.lowlevel.deployment; + +import org.jboss.jandex.DotName; + +import io.quarkiverse.opensearch.restclient.common.deployment.DevservicesOpenSearchBuildItem; +import io.quarkiverse.opensearch.restclient.lowlevel.OpenSearchClientConfig; +import io.quarkiverse.opensearch.restclient.lowlevel.runtime.OpenSearchRestClientProducer; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem; +import io.quarkus.arc.processor.DotNames; +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; + +class OpenSearchLowLevelClientProcessor { + + private static final DotName OPENSEARCH_CLIENT_CONFIG = DotName.createSimple(OpenSearchClientConfig.class.getName()); + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(Feature.ELASTICSEARCH_REST_CLIENT); + } + + @BuildStep + AdditionalBeanBuildItem build() { + return AdditionalBeanBuildItem.unremovableOf(OpenSearchRestClientProducer.class); + } + + @BuildStep + void openSearchClientConfigSupport(BuildProducer additionalBeans, + BuildProducer beanDefiningAnnotations) { + // add the @OpenSearchClientConfig class otherwise it won't be registered as a qualifier + additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClass(OpenSearchClientConfig.class).build()); + + beanDefiningAnnotations + .produce(new BeanDefiningAnnotationBuildItem(OPENSEARCH_CLIENT_CONFIG, DotNames.APPLICATION_SCOPED, false)); + } + + @BuildStep + HealthBuildItem addHealthCheck(OpenSearchBuildTimeConfig buildTimeConfig) { + return new HealthBuildItem("io.quarkiverse.opensearch.restclient.lowlevel.runtime.health.OpenSearchHealthCheck", + buildTimeConfig.healthEnabled); + } + + @BuildStep + DevservicesOpenSearchBuildItem devServices() { + return new DevservicesOpenSearchBuildItem("quarkiverse.opensearch.hosts"); + } + +} diff --git a/opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/DevServicesOpenSearchDevModeTestCase.java b/opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/DevServicesOpenSearchDevModeTestCase.java new file mode 100644 index 0000000..4134ba7 --- /dev/null +++ b/opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/DevServicesOpenSearchDevModeTestCase.java @@ -0,0 +1,34 @@ +package io.quarkiverse.opensearch.restclient.lowlevel.runtime; + +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class DevServicesOpenSearchDevModeTestCase { + @RegisterExtension + static QuarkusDevModeTest test = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClass(TestResource.class)); + + @Test + public void testDatasource() throws Exception { + var fruit = new TestResource.Fruit(); + fruit.id = "1"; + fruit.name = "banana"; + fruit.color = "yellow"; + + RestAssured + .given().body(fruit).contentType("application/json") + .when().post("/fruits") + .then().statusCode(204); + + RestAssured.when().get("/fruits/search?term=color&match=yellow") + .then() + .statusCode(200) + .body(equalTo("[{\"id\":\"1\",\"name\":\"banana\",\"color\":\"yellow\"}]")); + } +} diff --git a/opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchClientConfigTest.java b/opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchClientConfigTest.java new file mode 100644 index 0000000..8574724 --- /dev/null +++ b/opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchClientConfigTest.java @@ -0,0 +1,49 @@ +package io.quarkiverse.opensearch.restclient.lowlevel.runtime; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.opensearch.client.RestClientBuilder; + +import io.quarkiverse.opensearch.restclient.lowlevel.OpenSearchClientConfig; +import io.quarkus.test.QuarkusUnitTest; + +public class OpenSearchClientConfigTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class).addClasses(TestConfigurator.class, RestClientBuilderHelper.class) + .addAsResource(new StringAsset("quarkiverse.opensearch.hosts=opensearch:9200"), + "application.properties")); + + @Inject + OpenSearchConfig config; + + @Test + public void testRestClientBuilderHelperWithOpenSearchClientConfig() { + RestClientBuilderHelper.createRestClientBuilder(config).build(); + assertTrue(TestConfigurator.invoked); + } + + @OpenSearchClientConfig + @ApplicationScoped + public static class TestConfigurator implements RestClientBuilder.HttpClientConfigCallback { + + private static boolean invoked = false; + + @Override + public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder builder) { + invoked = true; + return builder; + } + } + +} diff --git a/opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/TestResource.java b/opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/TestResource.java new file mode 100644 index 0000000..e4a80a4 --- /dev/null +++ b/opensearch-rest-client/deployment/src/test/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/TestResource.java @@ -0,0 +1,65 @@ +package io.quarkiverse.opensearch.restclient.lowlevel.runtime; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; + +import org.apache.http.util.EntityUtils; +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.client.RestClient; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +@Path("/fruits") +public class TestResource { + @Inject + RestClient restClient; + + @POST + public void index(Fruit fruit) throws IOException { + Request request = new Request( + "PUT", + "/fruits/_doc/" + fruit.id + "?refresh=true"); + request.setJsonEntity(JsonObject.mapFrom(fruit).toString()); + restClient.performRequest(request); + } + + @GET + @Path("/search") + public List search(@QueryParam("term") String term, @QueryParam("match") String match) throws IOException { + Request request = new Request( + "GET", + "/fruits/_search"); + //construct a JSON query like {"query": {"match": {"": " results = new ArrayList<>(hits.size()); + for (int i = 0; i < hits.size(); i++) { + JsonObject hit = hits.getJsonObject(i); + Fruit fruit = hit.getJsonObject("_source").mapTo(Fruit.class); + results.add(fruit); + } + return results; + } + + public static class Fruit { + public String id; + public String name; + public String color; + } +} diff --git a/opensearch-rest-client/pom.xml b/opensearch-rest-client/pom.xml new file mode 100644 index 0000000..58f9c82 --- /dev/null +++ b/opensearch-rest-client/pom.xml @@ -0,0 +1,22 @@ + + + + io.quarkiverse.opensearch + quarkus-opensearch-parent + 0.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + + quarkus-opensearch-rest-client-parent + Quarkus - OpenSearch REST client - Parent + pom + + + deployment + runtime + + diff --git a/opensearch-rest-client/runtime/pom.xml b/opensearch-rest-client/runtime/pom.xml new file mode 100644 index 0000000..74105a1 --- /dev/null +++ b/opensearch-rest-client/runtime/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-parent + 0.1.0-SNAPSHOT + ../pom.xml + + + quarkus-opensearch-rest-client + Quarkus - OpenSearch REST client - Runtime + Connect to an OpenSearch cluster using the REST low level client + + + + io.quarkus + quarkus-core + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-common + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-smallrye-health + true + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/OpenSearchClientConfig.java b/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/OpenSearchClientConfig.java new file mode 100644 index 0000000..6408aca --- /dev/null +++ b/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/OpenSearchClientConfig.java @@ -0,0 +1,28 @@ +package io.quarkiverse.opensearch.restclient.lowlevel; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.util.AnnotationLiteral; +import javax.inject.Qualifier; + +/** + * Annotate implementations of {@code org.opensearch.client.RestClientBuilder.HttpClientConfigCallback} to provide further + * configuration of injected OpenSearch {@code RestClient} You may provide multiple implementations each annotated with + * {@code OpenSearchClientConfig} and configuration provided by each implementation will be applied in a randomly ordered + * cascading manner + */ +@Qualifier +@Target({ FIELD, TYPE, PARAMETER }) +@Retention(RUNTIME) +public @interface OpenSearchClientConfig { + + class Literal extends AnnotationLiteral implements OpenSearchClientConfig { + + } +} diff --git a/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchConfig.java b/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchConfig.java new file mode 100644 index 0000000..d2488b4 --- /dev/null +++ b/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchConfig.java @@ -0,0 +1,95 @@ +package io.quarkiverse.opensearch.restclient.lowlevel.runtime; + +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(prefix = "quarkiverse.opensearch") +public class OpenSearchConfig { + + /** + * The list of hosts of the OpenSearch servers. + */ + @ConfigItem(defaultValue = "localhost:9200") + public List hosts; + + /** + * The protocol to use when contacting OpenSearch servers. + * Set to "https" to enable SSL/TLS. + */ + @ConfigItem(defaultValue = "http") + public String protocol; + + /** + * The username for basic HTTP authentication. + */ + @ConfigItem + public Optional username; + + /** + * The password for basic HTTP authentication. + */ + @ConfigItem + public Optional password; + + /** + * The connection timeout. + */ + @ConfigItem(defaultValue = "1S") + public Duration connectionTimeout; + + /** + * The socket timeout. + */ + @ConfigItem(defaultValue = "30S") + public Duration socketTimeout; + + /** + * The maximum number of connections to all the OpenSearch servers. + */ + @ConfigItem(defaultValue = "20") + public int maxConnections; + + /** + * The maximum number of connections per OpenSearch server. + */ + @ConfigItem(defaultValue = "10") + public int maxConnectionsPerRoute; + + /** + * The number of IO thread. + * By default, this is the number of locally detected processors. + *

+ * Thread counts higher than the number of processors should not be necessary because the I/O threads rely on non-blocking + * operations, but you may want to use a thread count lower than the number of processors. + */ + @ConfigItem + public Optional ioThreadCounts; + + /** + * Configuration for the automatic discovery of new OpenSearch nodes. + */ + @ConfigItem + public DiscoveryConfig discovery; + + @ConfigGroup + public static class DiscoveryConfig { + + /** + * Defines if automatic discovery is enabled. + */ + @ConfigItem(defaultValue = "false") + public boolean enabled; + + /** + * Refresh interval of the node list. + */ + @ConfigItem(defaultValue = "5M") + public Duration refreshInterval; + } +} diff --git a/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchRestClientProducer.java b/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchRestClientProducer.java new file mode 100644 index 0000000..05bd722 --- /dev/null +++ b/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/OpenSearchRestClientProducer.java @@ -0,0 +1,52 @@ +package io.quarkiverse.opensearch.restclient.lowlevel.runtime; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.opensearch.client.RestClient; +import org.opensearch.client.RestClientBuilder; +import org.opensearch.client.sniff.Sniffer; + +@ApplicationScoped +public class OpenSearchRestClientProducer { + + @Inject + OpenSearchConfig config; + + private RestClient client; + + private Sniffer sniffer; + + @Produces + @Singleton + public RestClient restClient() { + RestClientBuilder builder = RestClientBuilderHelper.createRestClientBuilder(config); + + this.client = builder.build(); + if (config.discovery.enabled) { + this.sniffer = RestClientBuilderHelper.createSniffer(client, config); + } + + return this.client; + } + + @PreDestroy + void destroy() { + try { + if (this.sniffer != null) { + this.sniffer.close(); + } + if (this.client != null) { + this.client.close(); + } + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } +} diff --git a/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/RestClientBuilderHelper.java b/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/RestClientBuilderHelper.java new file mode 100644 index 0000000..2c6838d --- /dev/null +++ b/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/RestClientBuilderHelper.java @@ -0,0 +1,115 @@ +package io.quarkiverse.opensearch.restclient.lowlevel.runtime; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; + +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.apache.http.impl.nio.reactor.IOReactorConfig; +import org.apache.http.nio.conn.NoopIOSessionStrategy; +import org.jboss.logging.Logger; +import org.opensearch.client.RestClient; +import org.opensearch.client.RestClientBuilder; +import org.opensearch.client.sniff.NodesSniffer; +import org.opensearch.client.sniff.OpenSearchNodesSniffer; +import org.opensearch.client.sniff.Sniffer; +import org.opensearch.client.sniff.SnifferBuilder; + +import io.quarkiverse.opensearch.restclient.lowlevel.OpenSearchClientConfig; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; + +public final class RestClientBuilderHelper { + + private static final Logger LOG = Logger.getLogger(RestClientBuilderHelper.class); + + private RestClientBuilderHelper() { + // avoid instantiation + } + + public static RestClientBuilder createRestClientBuilder(OpenSearchConfig config) { + List hosts = new ArrayList<>(config.hosts.size()); + for (InetSocketAddress host : config.hosts) { + hosts.add(new HttpHost(host.getHostString(), host.getPort(), config.protocol)); + } + + RestClientBuilder builder = RestClient.builder(hosts.toArray(new HttpHost[0])); + + builder.setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() { + @Override + public RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder requestConfigBuilder) { + return requestConfigBuilder + .setConnectTimeout((int) config.connectionTimeout.toMillis()) + .setSocketTimeout((int) config.socketTimeout.toMillis()) + .setConnectionRequestTimeout(0); // Avoid requests being flagged as timed out even when they didn't time out. + } + }); + + builder.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() { + @Override + public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) { + if (config.username.isPresent()) { + if (!"https".equalsIgnoreCase(config.protocol)) { + LOG.warn("Using Basic authentication in HTTP implies sending plain text passwords over the wire, " + + "use the HTTPS protocol instead."); + } + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials(config.username.get(), config.password.orElse(null))); + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); + } + + if (config.ioThreadCounts.isPresent()) { + IOReactorConfig ioReactorConfig = IOReactorConfig.custom() + .setIoThreadCount(config.ioThreadCounts.get()) + .build(); + httpClientBuilder.setDefaultIOReactorConfig(ioReactorConfig); + } + + httpClientBuilder.setMaxConnTotal(config.maxConnections); + httpClientBuilder.setMaxConnPerRoute(config.maxConnectionsPerRoute); + + if ("http".equalsIgnoreCase(config.protocol)) { + // In this case disable the SSL capability as it might have an impact on + // bootstrap time, for example consuming entropy for no reason + httpClientBuilder.setSSLStrategy(NoopIOSessionStrategy.INSTANCE); + } + + // Apply configuration from RestClientBuilder.HttpClientConfigCallback implementations annotated with OpenSearchClientConfig + HttpAsyncClientBuilder result = httpClientBuilder; + Iterable> handles = Arc.container() + .select(RestClientBuilder.HttpClientConfigCallback.class, new OpenSearchClientConfig.Literal()) + .handles(); + for (InstanceHandle handle : handles) { + result = handle.get().customizeHttpClient(result); + handle.close(); + } + return result; + } + }); + + return builder; + } + + public static Sniffer createSniffer(RestClient client, OpenSearchConfig config) { + SnifferBuilder builder = Sniffer.builder(client) + .setSniffIntervalMillis((int) config.discovery.refreshInterval.toMillis()); + + // https discovery support + if ("https".equalsIgnoreCase(config.protocol)) { + NodesSniffer hostsSniffer = new OpenSearchNodesSniffer( + client, + OpenSearchNodesSniffer.DEFAULT_SNIFF_REQUEST_TIMEOUT, // 1sec + OpenSearchNodesSniffer.Scheme.HTTPS); + builder.setNodesSniffer(hostsSniffer); + } + + return builder.build(); + } +} diff --git a/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/health/OpenSearchHealthCheck.java b/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/health/OpenSearchHealthCheck.java new file mode 100644 index 0000000..4e98a99 --- /dev/null +++ b/opensearch-rest-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/lowlevel/runtime/health/OpenSearchHealthCheck.java @@ -0,0 +1,43 @@ +package io.quarkiverse.opensearch.restclient.lowlevel.runtime.health; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.http.util.EntityUtils; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; +import org.eclipse.microprofile.health.Readiness; +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.client.RestClient; + +import io.vertx.core.json.JsonObject; + +@Readiness +@ApplicationScoped +public class OpenSearchHealthCheck implements HealthCheck { + @Inject + RestClient restClient; + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder builder = HealthCheckResponse.named("OpenSearch cluster health check").up(); + try { + Request request = new Request("GET", "/_cluster/health"); + Response response = restClient.performRequest(request); + String responseBody = EntityUtils.toString(response.getEntity()); + JsonObject json = new JsonObject(responseBody); + String status = json.getString("status"); + if ("red".equals(status)) { + builder.down().withData("status", status); + } else { + builder.up().withData("status", status); + } + + } catch (Exception e) { + return builder.down().withData("reason", e.getMessage()).build(); + } + return builder.build(); + } +} diff --git a/opensearch-rest-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/opensearch-rest-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000..baf3993 --- /dev/null +++ b/opensearch-rest-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,13 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "OpenSearch REST client" +metadata: + keywords: + - "opensearch" + - "full text" + - "search" + categories: + - "data" + status: "stable" + config: + - "quarkiverse.opensearch." diff --git a/opensearch-rest-high-level-client/deployment/pom.xml b/opensearch-rest-high-level-client/deployment/pom.xml new file mode 100644 index 0000000..ed25668 --- /dev/null +++ b/opensearch-rest-high-level-client/deployment/pom.xml @@ -0,0 +1,58 @@ + + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-high-level-client-parent + 0.1.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-opensearch-rest-high-level-client-deployment + Quarkus - OpenSearch REST high level client - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-common-deployment + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-deployment + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-high-level-client + + + io.quarkus + quarkus-arc-deployment + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + + + \ No newline at end of file diff --git a/opensearch-rest-high-level-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/deployment/OpenSearchHighLevelClientProcessor.java b/opensearch-rest-high-level-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/deployment/OpenSearchHighLevelClientProcessor.java new file mode 100644 index 0000000..00ce09d --- /dev/null +++ b/opensearch-rest-high-level-client/deployment/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/deployment/OpenSearchHighLevelClientProcessor.java @@ -0,0 +1,21 @@ +package io.quarkiverse.opensearch.restclient.highlevel.deployment; + +import io.quarkiverse.opensearch.restclient.highlevel.runtime.OpenSearchRestHighLevelClientProducer; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +class OpenSearchHighLevelClientProcessor { + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(Feature.ELASTICSEARCH_REST_HIGH_LEVEL_CLIENT); + } + + @BuildStep() + AdditionalBeanBuildItem build() { + return AdditionalBeanBuildItem.unremovableOf(OpenSearchRestHighLevelClientProducer.class); + } + +} diff --git a/opensearch-rest-high-level-client/pom.xml b/opensearch-rest-high-level-client/pom.xml new file mode 100644 index 0000000..3ac5abd --- /dev/null +++ b/opensearch-rest-high-level-client/pom.xml @@ -0,0 +1,23 @@ + + + + io.quarkiverse.opensearch + quarkus-opensearch-parent + 0.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + + quarkus-opensearch-rest-high-level-client-parent + Quarkus - OpenSearch REST high level client - Parent + pom + + + runtime + deployment + + + \ No newline at end of file diff --git a/opensearch-rest-high-level-client/runtime/pom.xml b/opensearch-rest-high-level-client/runtime/pom.xml new file mode 100644 index 0000000..61560af --- /dev/null +++ b/opensearch-rest-high-level-client/runtime/pom.xml @@ -0,0 +1,121 @@ + + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-high-level-client-parent + 0.1.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-opensearch-rest-high-level-client + Quarkus - OpenSearch REST high level client - Runtime + Connect to an OpenSearch cluster using the REST high level client + + + + io.quarkus + quarkus-core + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-common + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client + + + io.quarkus + quarkus-arc + + + org.jboss.logmanager + log4j2-jboss-logmanager + + + org.opensearch.client + opensearch-rest-high-level-client + + + + + + org.apache.lucene + lucene-analyzers-common + + + org.apache.lucene + lucene-backward-codecs + + + org.apache.lucene + lucene-grouping + + + org.apache.lucene + lucene-memory + + + org.apache.lucene + lucene-misc + + + org.apache.lucene + lucene-queryparser + + + org.apache.lucene + lucene-sandbox + + + org.apache.lucene + lucene-spatial + + + org.apache.lucene + lucene-spatial-extras + + + org.apache.lucene + lucene-spatial3d + + + org.apache.lucene + lucene-suggest + + + + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + io.quarkiverse.opensearch-rest-high-level-client + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + + diff --git a/opensearch-rest-high-level-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/runtime/OpenSearchRestHighLevelClientProducer.java b/opensearch-rest-high-level-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/runtime/OpenSearchRestHighLevelClientProducer.java new file mode 100644 index 0000000..97c9a9a --- /dev/null +++ b/opensearch-rest-high-level-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/runtime/OpenSearchRestHighLevelClientProducer.java @@ -0,0 +1,43 @@ +package io.quarkiverse.opensearch.restclient.highlevel.runtime; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Collections; + +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Default; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.opensearch.client.RestClient; +import org.opensearch.client.RestHighLevelClient; + +@ApplicationScoped +public class OpenSearchRestHighLevelClientProducer { + + @Inject + @Default + RestClient restClient; + + private RestHighLevelClient client; + + @Produces + @Singleton + public RestHighLevelClient restHighLevelClient() { + this.client = new QuarkusRestHighLevelClient(restClient, RestClient::close, Collections.emptyList()); + return this.client; + } + + @PreDestroy + void destroy() { + try { + if (this.client != null) { + this.client.close(); + } + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } +} diff --git a/opensearch-rest-high-level-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/runtime/QuarkusRestHighLevelClient.java b/opensearch-rest-high-level-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/runtime/QuarkusRestHighLevelClient.java new file mode 100644 index 0000000..6c4bfaa --- /dev/null +++ b/opensearch-rest-high-level-client/runtime/src/main/java/io/quarkiverse/opensearch/restclient/highlevel/runtime/QuarkusRestHighLevelClient.java @@ -0,0 +1,22 @@ +package io.quarkiverse.opensearch.restclient.highlevel.runtime; + +import java.io.IOException; +import java.util.List; + +import org.opensearch.client.RestClient; +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.common.CheckedConsumer; +import org.opensearch.common.xcontent.NamedXContentRegistry; + +/** + * The RestHighLevelClient cannot be built with an existing RestClient. + *

+ * The only (and documented - see javadoc) way to do it is to subclass it and use its protected constructor. + */ +class QuarkusRestHighLevelClient extends RestHighLevelClient { + + protected QuarkusRestHighLevelClient(RestClient restClient, CheckedConsumer doClose, + List namedXContentEntries) { + super(restClient, doClose, namedXContentEntries); + } +} diff --git a/opensearch-rest-high-level-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/opensearch-rest-high-level-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000..b27fae1 --- /dev/null +++ b/opensearch-rest-high-level-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,12 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "OpenSearch REST High Level Client" +metadata: + keywords: + - "opensearch" + - "full text" + - "search" + categories: + - "data" + config: + - "quarkiverse.opensearch." diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d62b228 --- /dev/null +++ b/pom.xml @@ -0,0 +1,133 @@ + + + 4.0.0 + + io.quarkiverse + quarkiverse-parent + 12 + + io.quarkiverse.opensearch + quarkus-opensearch-parent + 0.1.0-SNAPSHOT + pom + Quarkus - OpenSearch Client - Parent + + opensearch-rest-client-common + opensearch-rest-client + opensearch-rest-high-level-client + + + 3.8.1 + true + 11 + 11 + UTF-8 + UTF-8 + 2.4.1 + 2.0.0 + 2.15.0.Final + + + + + io.quarkus + quarkus-bom + ${quarkus.version} + pom + import + + + + org.opensearch.client + opensearch-rest-client + ${opensearch.version} + + + org.opensearch.client + opensearch-rest-high-level-client + ${opensearch.version} + + + + org.apache.logging.log4j + log4j-api + + + + + org.opensearch.client + opensearch-rest-client-sniffer + ${opensearch.version} + + + org.opensearch + opensearch-testcontainers + ${opensearch-testcontainers.version} + + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-common + ${project.version} + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-common-deployment + ${project.version} + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client + ${project.version} + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-client-deployment + ${project.version} + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-high-level-client + ${project.version} + + + io.quarkiverse.opensearch + quarkus-opensearch-rest-high-level-client-deployment + ${project.version} + + + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.version} + + + maven-compiler-plugin + ${compiler-plugin.version} + + + + + + + it + + + performRelease + !true + + + + + +