This is an automated email from the ASF dual-hosted git repository. shuber pushed a commit to branch opensearch-persistence in repository https://gitbox.apache.org/repos/asf/unomi.git
commit 3827c24802d70cf69f80591198dafd5d8e49769b Author: Serge Huber <shu...@jahia.com> AuthorDate: Fri Jan 3 19:43:50 2025 +0100 - Add support for OpenSearch in docker images - Add docker compose support for OpenSearch - Fix startup issues with updates to UnomiManagementService - Documentation updates to add OpenSearch information (still to be completed) --- docker/README.md | 63 +++++++++-- docker/pom.xml | 27 +++-- docker/src/main/docker/Dockerfile | 15 ++- ...mpose-build.yml => docker-compose-build-es.yml} | 7 ++ docker/src/main/docker/docker-compose-build-os.yml | 122 +++++++++++++++++++++ .../{docker-compose.yml => docker-compose-es.yml} | 23 +++- docker/src/main/docker/docker-compose-os.yml | 122 +++++++++++++++++++++ docker/src/main/docker/entrypoint.sh | 112 ++++++++++++++++--- itests/README.md | 9 +- manual/src/main/asciidoc/configuration.adoc | 34 +++++- package/pom.xml | 17 ++- .../shell/migration/service/MigrationConfig.java | 3 +- .../shell/services/UnomiManagementService.java | 20 +++- .../internal/UnomiManagementServiceImpl.java | 105 +++++++++++++++--- 14 files changed, 614 insertions(+), 65 deletions(-) diff --git a/docker/README.md b/docker/README.md index 68b6034b5..b6dce3c4f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -30,16 +30,16 @@ required Unomi tarball. ## Launching docker-compose using Maven project -Unomi requires ElasticSearch so it is recommended to run Unomi and ElasticSearch using docker-compose: +Unomi requires a search engine (ElasticSearch or OpenSearch) so it is recommended to run Unomi and the search engine using docker-compose: ``` mvn docker:start ``` -You will need to wait while Docker builds the containers and they boot up (ES will take a minute or two). Once they are +You will need to wait while Docker builds the containers and they boot up (the search engine will take a minute or two). Once they are up you can check that the Unomi services are available by visiting http://localhost:8181 in a web browser. -## Manually launching ElasticSearch & Unomi docker images +## Manually launching Search Engine & Unomi docker images If you want to run it without docker-compose you should then make sure you setup the following environments properly. @@ -48,21 +48,66 @@ For ElasticSearch: docker pull docker.elastic.co/elasticsearch/elasticsearch:7.4.2 docker network create unomi docker run --name elasticsearch --net unomi -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e cluster.name=contextElasticSearch docker.elastic.co/elasticsearch/elasticsearch:7.4.2 + +For OpenSearch: + + docker pull opensearchproject/opensearch:2.18.0 + docker network create unomi + export OPENSEARCH_ADMIN_PASSWORD=enter_your_custom_admin_password_here + docker run --name opensearch --net unomi -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} opensearchproject/opensearch:2.18.0 -For Unomi: +For Unomi (with ElasticSearch): + + docker pull apache/unomi:2.7.0-SNAPSHOT + docker run --name unomi --net unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \ + -e UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200 \ + apache/unomi:2.7.0-SNAPSHOT - docker pull apache/unomi:2.0.0-SNAPSHOT - docker run --name unomi --net unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 -e UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200 apache/unomi:2.0.0-SNAPSHOT +For Unomi (with OpenSearch): -## Using a host OS ElasticSearch installation (only supported on macOS & Windows) + docker pull apache/unomi:2.7.0-SNAPSHOT + docker run --name unomi --net unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \ + -e UNOMI_AUTO_START=opensearch \ + -e UNOMI_OPENSEARCH_ADDRESSES=opensearch:9200 \ + -e UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} \ + apache/unomi:2.7.0-SNAPSHOT + +## Using a host OS Search Engine installation (only supported on macOS & Windows) + +For ElasticSearch: - docker run --name unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 -e UNOMI_ELASTICSEARCH_ADDRESSES=host.docker.internal:9200 apache/unomi:2.0.0-SNAPSHOT + docker run --name unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \ + -e UNOMI_ELASTICSEARCH_ADDRESSES=host.docker.internal:9200 \ + apache/unomi:2.7.0-SNAPSHOT + +For OpenSearch: + + docker run --name unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \ + -e UNOMI_AUTO_START=opensearch \ + -e UNOMI_OPENSEARCH_ADDRESSES=host.docker.internal:9200 \ + -e UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} \ + apache/unomi:2.7.0-SNAPSHOT Note: Linux doesn't support the host.docker.internal DNS lookup method yet, it should be available in an upcoming version of Docker. See https://github.com/docker/for-linux/issues/264 +## Environment Variables + +### Common Variables +- `UNOMI_AUTO_START`: Specifies the search engine type (`elasticsearch` or `opensearch`, defaults to `elasticsearch`) + +### ElasticSearch-specific Variables +- `UNOMI_ELASTICSEARCH_ADDRESSES`: ElasticSearch host:port (default: localhost:9200) +- `UNOMI_ELASTICSEARCH_USERNAME`: Optional username for ElasticSearch +- `UNOMI_ELASTICSEARCH_PASSWORD`: Optional password for ElasticSearch +- `UNOMI_ELASTICSEARCH_SSL_ENABLE`: Enable SSL for ElasticSearch connection (default: false) + +### OpenSearch-specific Variables +- `UNOMI_OPENSEARCH_ADDRESSES`: OpenSearch host:port (default: localhost:9200) +- `UNOMI_OPENSEARCH_PASSWORD`: Required admin password for OpenSearch (SSL and authentication are mandatory) + # Using docker build tools If you want to rebuild the images or use docker compose directly, you must still first use `mvn clean install` to generate the filtered project in `target/filtered-docker`. -You can then use `docker-compose up` to start the project +You can then use `docker compose -f docker-compose-es.yml up` to start the project with ElasticSearch or `docker compose -f docker-compose-os.yml up` to start the project with OpenSearch. diff --git a/docker/pom.xml b/docker/pom.xml index 57502de25..bc1d64af2 100644 --- a/docker/pom.xml +++ b/docker/pom.xml @@ -83,8 +83,10 @@ <directory>${project.basedir}/src/main/docker</directory> <filtering>true</filtering> <includes> - <include>docker-compose.yml</include> - <include>docker-compose-build.yml</include> + <include>docker-compose-es.yml</include> + <include>docker-compose-build-es.yml</include> + <include>docker-compose-os.yml</include> + <include>docker-compose-build-os.yml</include> </includes> </resource> <!-- # Unfiltered Resources --> @@ -92,8 +94,10 @@ <directory>${project.basedir}/src/main/docker</directory> <filtering>false</filtering> <excludes> - <exclude>docker-compose.yml</exclude> - <include>docker-compose-build.yml</include> + <exclude>docker-compose-es.yml</exclude> + <exclude>docker-compose-build-es.yml</exclude> + <exclude>docker-compose-os.yml</exclude> + <exclude>docker-compose-build-os.yml</exclude> </excludes> </resource> </resources> @@ -105,7 +109,7 @@ <plugin> <groupId>io.fabric8</groupId> <artifactId>docker-maven-plugin</artifactId> - <version>0.40.2</version> + <version>0.45.1</version> <configuration> <images> <image> @@ -122,7 +126,7 @@ <external> <type>compose</type> <basedir>${project.build.directory}/filtered-docker</basedir> - <composeFile>${project.build.directory}/filtered-docker/docker-compose-build.yml</composeFile> + <composeFile>${project.build.directory}/filtered-docker/docker-compose-build-es.yml</composeFile> </external> </image> </images> @@ -150,10 +154,17 @@ <artifacts> <artifact> <file> - ${project.build.directory}/filtered-docker/docker-compose.yml + ${project.build.directory}/filtered-docker/docker-compose-es.yml </file> <type>yml</type> - <classifier>docker-compose</classifier> + <classifier>docker-compose-es</classifier> + </artifact> + <artifact> + <file> + ${project.build.directory}/filtered-docker/docker-compose-os.yml + </file> + <type>yml</type> + <classifier>docker-compose-os</classifier> </artifact> </artifacts> </configuration> diff --git a/docker/src/main/docker/Dockerfile b/docker/src/main/docker/Dockerfile index 6734689fc..377bc9ff2 100644 --- a/docker/src/main/docker/Dockerfile +++ b/docker/src/main/docker/Dockerfile @@ -18,12 +18,18 @@ FROM library/eclipse-temurin:11 # Unomi environment variables -ENV UNOMI_HOME /opt/apache-unomi -ENV PATH $PATH:$UNOMI_HOME/bin +ENV UNOMI_HOME=/opt/apache-unomi +ENV PATH=$PATH:$UNOMI_HOME/bin -ENV KARAF_OPTS "-Dunomi.autoStart=true" +ENV UNOMI_AUTO_START=true + +# Debug configuration (disabled by default) +ENV KARAF_DEBUG=false +ENV KARAF_DEBUG_PORT=5005 +ENV KARAF_DEBUG_SUSPEND=n ENV UNOMI_ELASTICSEARCH_ADDRESSES=localhost:9200 +ENV UNOMI_OPENSEARCH_ADDRESSES=localhost:9200 WORKDIR $UNOMI_HOME @@ -36,8 +42,11 @@ RUN mv unomi-*/* . \ COPY entrypoint.sh ./entrypoint.sh +# Expose standard ports EXPOSE 9443 EXPOSE 8181 EXPOSE 8102 +# Expose debug port +EXPOSE 5005 CMD ["/bin/bash", "./entrypoint.sh"] diff --git a/docker/src/main/docker/docker-compose-build.yml b/docker/src/main/docker/docker-compose-build-es.yml similarity index 88% rename from docker/src/main/docker/docker-compose-build.yml rename to docker/src/main/docker/docker-compose-build-es.yml index 58a8f6a14..eaf415083 100644 --- a/docker/src/main/docker/docker-compose-build.yml +++ b/docker/src/main/docker/docker-compose-build-es.yml @@ -34,11 +34,18 @@ services: image: apache/unomi:${project.version} container_name: unomi environment: + - UNOMI_AUTO_START=elasticsearch - UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200 + # Debug settings + - KARAF_DEBUG=${DEBUG:-false} + - KARAF_DEBUG_PORT=${DEBUG_PORT:-5005} + - KARAF_DEBUG_SUSPEND=${DEBUG_SUSPEND:-n} ports: - 8181:8181 - 9443:9443 - 8102:8102 + # Debug port + - "${DEBUG_PORT:-5005}:5005" links: - elasticsearch depends_on: diff --git a/docker/src/main/docker/docker-compose-build-os.yml b/docker/src/main/docker/docker-compose-build-os.yml new file mode 100644 index 000000000..2d20c5fd4 --- /dev/null +++ b/docker/src/main/docker/docker-compose-build-os.yml @@ -0,0 +1,122 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ +version: '2.4' + +# Define networks first +networks: + unomi-net: + driver: bridge + +services: + opensearch-node1: + image: opensearchproject/opensearch:2.18.0 + container_name: opensearch-node1 + environment: + - cluster.name=opensearch-cluster + - node.name=opensearch-node1 + - discovery.seed_hosts=opensearch-node1,opensearch-node2 + - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 + - bootstrap.memory_lock=true + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" + - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + volumes: + - opensearch-data1:/usr/share/opensearch/data + ports: + - 9200:9200 + - 9600:9600 + networks: + unomi-net: + aliases: + - opensearch-node1 + + opensearch-node2: + image: opensearchproject/opensearch:2.18.0 + container_name: opensearch-node2 + environment: + - cluster.name=opensearch-cluster + - node.name=opensearch-node2 + - discovery.seed_hosts=opensearch-node1,opensearch-node2 + - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 + - bootstrap.memory_lock=true + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" + - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + volumes: + - opensearch-data2:/usr/share/opensearch/data + networks: + unomi-net: + aliases: + - opensearch-node2 + + opensearch-dashboards: + image: opensearchproject/opensearch-dashboards:2.18.0 + container_name: opensearch-dashboards + ports: + - 5601:5601 + environment: + OPENSEARCH_HOSTS: '["https://opensearch-node1:9200","https://opensearch-node2:9200"]' + networks: + unomi-net: + aliases: + - opensearch-dashboards + depends_on: + - opensearch-node1 + - opensearch-node2 + + unomi: + build: . + image: apache/unomi:${project.version} + container_name: unomi + environment: + - UNOMI_AUTO_START=opensearch + - UNOMI_OPENSEARCH_ADDRESSES=opensearch-node1:9200 + - UNOMI_OPENSEARCH_USERNAME=admin + - UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} + # Debug settings + - KARAF_DEBUG=${DEBUG:-false} + - KARAF_DEBUG_PORT=${DEBUG_PORT:-5005} + - KARAF_DEBUG_SUSPEND=${DEBUG_SUSPEND:-n} + ports: + - 8181:8181 + - 9443:9443 + - 8102:8102 + # Debug port + - "${DEBUG_PORT:-5005}:5005" + depends_on: + - opensearch-node1 + - opensearch-node2 + networks: + unomi-net: + aliases: + - unomi + +volumes: + opensearch-data1: + opensearch-data2: diff --git a/docker/src/main/docker/docker-compose.yml b/docker/src/main/docker/docker-compose-es.yml similarity index 78% rename from docker/src/main/docker/docker-compose.yml rename to docker/src/main/docker/docker-compose-es.yml index fc8679033..dbd931ec9 100644 --- a/docker/src/main/docker/docker-compose.yml +++ b/docker/src/main/docker/docker-compose-es.yml @@ -15,9 +15,16 @@ # limitations under the License. ################################################################################ version: '2.4' + +# Define networks first +networks: + unomi-net: + driver: bridge + services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.4.2 + container_name: elasticsearch volumes: # Persist ES data in separate "esdata" volume - esdata1:/usr/share/elasticsearch/data environment: @@ -28,21 +35,35 @@ services: - cluster.name=contextElasticSearch ports: # Expose Elasticsearch ports - "9200:9200" + networks: + unomi-net: + aliases: + - elasticsearch unomi: image: apache/unomi:${project.version} container_name: unomi environment: + - UNOMI_AUTO_START=elasticsearch - UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200 + # Debug settings + - KARAF_DEBUG=${DEBUG:-false} + - KARAF_DEBUG_PORT=${DEBUG_PORT:-5005} + - KARAF_DEBUG_SUSPEND=${DEBUG_SUSPEND:-n} ports: - 8181:8181 - 9443:9443 - 8102:8102 + # Debug port + - "${DEBUG_PORT:-5005}:5005" links: - elasticsearch depends_on: - elasticsearch - + networks: + unomi-net: + aliases: + - unomi volumes: # Define separate volume for Elasticsearch data esdata1: diff --git a/docker/src/main/docker/docker-compose-os.yml b/docker/src/main/docker/docker-compose-os.yml new file mode 100644 index 000000000..00de665ef --- /dev/null +++ b/docker/src/main/docker/docker-compose-os.yml @@ -0,0 +1,122 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ +version: '2.4' + +# Define networks first +networks: + unomi-net: + driver: bridge + +services: + opensearch-node1: + image: opensearchproject/opensearch:2.18.0 + container_name: opensearch-node1 + environment: + - cluster.name=opensearch-cluster + - node.name=opensearch-node1 + - discovery.seed_hosts=opensearch-node1,opensearch-node2 + - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 + - bootstrap.memory_lock=true + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" + - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + volumes: + - opensearch-data1:/usr/share/opensearch/data + ports: + - 9200:9200 + - 9600:9600 + networks: + unomi-net: + aliases: + - opensearch-node1 + + opensearch-node2: + image: opensearchproject/opensearch:2.18.0 + container_name: opensearch-node2 + environment: + - cluster.name=opensearch-cluster + - node.name=opensearch-node2 + - discovery.seed_hosts=opensearch-node1,opensearch-node2 + - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 + - bootstrap.memory_lock=true + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" + - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + volumes: + - opensearch-data2:/usr/share/opensearch/data + networks: + unomi-net: + aliases: + - opensearch-node2 + + unomi: + image: apache/unomi:${project.version} + container_name: unomi + environment: + - UNOMI_AUTO_START=opensearch + - UNOMI_OPENSEARCH_ADDRESSES=opensearch-node1:9200 + - UNOMI_OPENSEARCH_USERNAME=admin + - UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} + # Debug settings + - KARAF_DEBUG=${DEBUG:-false} + - KARAF_DEBUG_PORT=${DEBUG_PORT:-5005} + - KARAF_DEBUG_SUSPEND=${DEBUG_SUSPEND:-n} + ports: + - 8181:8181 + - 9443:9443 + - 8102:8102 + # Debug port + - "${DEBUG_PORT:-5005}:5005" + depends_on: + - opensearch-node1 + - opensearch-node2 + networks: + unomi-net: + aliases: + - unomi + + opensearch-dashboards: + image: opensearchproject/opensearch-dashboards:2.18.0 + container_name: opensearch-dashboards + ports: + - 5601:5601 + environment: + OPENSEARCH_HOSTS: '["https://opensearch-node1:9200","https://opensearch-node2:9200"]' + networks: + unomi-net: + aliases: + - opensearch-dashboards + depends_on: + - opensearch-node1 + - opensearch-node2 + +volumes: + opensearch-data1: + opensearch-data2: + diff --git a/docker/src/main/docker/entrypoint.sh b/docker/src/main/docker/entrypoint.sh index c601be4b3..45b556c6c 100755 --- a/docker/src/main/docker/entrypoint.sh +++ b/docker/src/main/docker/entrypoint.sh @@ -16,28 +16,110 @@ # See the License for the specific language governing permissions and # limitations under the License. ################################################################################ -# Wait for healthy ElasticSearch -# next wait for ES status to turn to Green -if [ "$UNOMI_ELASTICSEARCH_SSL_ENABLE" == 'true' ]; then - schema='https' +# Near the top of the file, add these default debug settings +KARAF_DEBUG=${KARAF_DEBUG:-false} +KARAF_DEBUG_PORT=${KARAF_DEBUG_PORT:-5005} +KARAF_DEBUG_SUSPEND=${KARAF_DEBUG_SUSPEND:-n} + +# Before starting Karaf, add debug configuration +if [ "$KARAF_DEBUG" = "true" ]; then + echo "Enabling Karaf debug mode on port $KARAF_DEBUG_PORT (suspend=$KARAF_DEBUG_SUSPEND)" + export JAVA_DEBUG_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=$KARAF_DEBUG_SUSPEND,address=*:$KARAF_DEBUG_PORT" + export KARAF_DEBUG=true +fi + +# Determine search engine type from UNOMI_AUTO_START +SEARCH_ENGINE="${UNOMI_AUTO_START:-elasticsearch}" +export KARAF_OPTS="-Dunomi.autoStart=${UNOMI_AUTO_START}" + +if [ "$SEARCH_ENGINE" = "true" ]; then + SEARCH_ENGINE="elasticsearch" +fi +echo "SEARCH_ENGINE: $SEARCH_ENGINE" +echo "KARAF_OPTS: $KARAF_OPTS" + +# Function to check cluster health for a specific node +check_node_health() { + local node_url="$1" + local curl_opts="$2" + response=$(eval curl -v -fsSL ${curl_opts} "${node_url}" 2>&1) + if [ $? -eq 0 ]; then + echo "$response" | grep -o '"status"[ ]*:[ ]*"[^"]*"' | cut -d'"' -f4 + else + echo "" + fi +} + +# Configure connection parameters based on search engine type +if [ "$SEARCH_ENGINE" = "opensearch" ]; then + # OpenSearch configuration + if [ -z "$UNOMI_OPENSEARCH_PASSWORD" ]; then + echo "Error: UNOMI_OPENSEARCH_PASSWORD must be set when using OpenSearch" + exit 1 + fi + + schema='https' + auth_header="Authorization: Basic $(echo -n "admin:${UNOMI_OPENSEARCH_PASSWORD}" | base64)" + health_endpoint="_cluster/health" + curl_opts="-k -H \"${auth_header}\" -H \"Content-Type: application/json\"" else - schema='http' + # Elasticsearch configuration + if [ "$UNOMI_ELASTICSEARCH_SSL_ENABLE" = 'true' ]; then + schema='https' + else + schema='http' + fi + + if [ -v UNOMI_ELASTICSEARCH_USERNAME ] && [ -v UNOMI_ELASTICSEARCH_PASSWORD ]; then + auth_header="Authorization: Basic $(echo -n "${UNOMI_ELASTICSEARCH_USERNAME}:${UNOMI_ELASTICSEARCH_PASSWORD}" | base64)" + curl_opts="-H \"${auth_header}\"" + fi + health_endpoint="_cluster/health" fi -if [ -v UNOMI_ELASTICSEARCH_USERNAME ] && [ -v UNOMI_ELASTICSEARCH_PASSWORD ]; then - elasticsearch_addresses="$schema://$UNOMI_ELASTICSEARCH_USERNAME:$UNOMI_ELASTICSEARCH_PASSWORD@$UNOMI_ELASTICSEARCH_ADDRESSES/_cat/health?h=status" +# Build array of node URLs +if [ "$SEARCH_ENGINE" = "opensearch" ]; then + IFS=',' read -ra NODES <<< "${UNOMI_OPENSEARCH_ADDRESSES}" else - elasticsearch_addresses="$schema://$UNOMI_ELASTICSEARCH_ADDRESSES/_cat/health?h=status" + IFS=',' read -ra NODES <<< "${UNOMI_ELASTICSEARCH_ADDRESSES}" fi -health_check="$(curl -fsSL "$elasticsearch_addresses")" +# Wait for search engine to be ready +echo "Waiting for ${SEARCH_ENGINE} to be ready..." +echo "Checking nodes: ${NODES[@]}" +health_check="" + +while ([ -z "$health_check" ] || ([ "$health_check" != 'yellow' ] && [ "$health_check" != 'green' ])); do + # Try each node until we get a successful response + for node in "${NODES[@]}"; do + node_url="${schema}://${node}/${health_endpoint}" + echo "Checking health at: ${node_url}" + health_check=$(check_node_health "$node_url" "$curl_opts") -until ([ "$health_check" = 'yellow' ] || [ "$health_check" = 'green' ]); do - health_check="$(curl -fsSL $elasticsearch_addresses)" - >&2 echo "Elastic Search is not yet available - waiting (health check=$health_check)..." - sleep 1 + if [ ! -z "$health_check" ]; then + echo "Successfully connected to node: $node (status: ${health_check})" + break + else + >&2 echo "Connection failed to node: $node" + fi + done + + if [ -z "$health_check" ]; then + >&2 echo "${SEARCH_ENGINE^} is not yet available - all nodes unreachable" + sleep 3 + continue + fi + + if [ "$health_check" != 'yellow' ] && [ "$health_check" != 'green' ]; then + >&2 echo "${SEARCH_ENGINE^} health status: ${health_check} (waiting for yellow or green)" + sleep 3 + else + >&2 echo "${SEARCH_ENGINE^} health status: ${health_check}" + fi done -# Run Unomi in current bash session, if Unomi crash or shutdown the container will stop -$UNOMI_HOME/bin/karaf run +echo "${SEARCH_ENGINE^} is ready with health status: ${health_check}" + +# Run Unomi in current bash session +exec "$UNOMI_HOME/bin/karaf" run diff --git a/itests/README.md b/itests/README.md index ad9f77f82..8098d2901 100644 --- a/itests/README.md +++ b/itests/README.md @@ -58,7 +58,7 @@ from the project's root directory ### Search Engine Selection -The integration tests can be run against either ElasticSearch (default) or OpenSearch: +Apache Unomi supports both ElasticSearch and OpenSearch as search engine backends. The integration tests can be configured to run against either engine: ```bash # Run with ElasticSearch (default) @@ -68,6 +68,13 @@ mvn clean install -P integration-tests mvn clean install -P integration-tests -Duse.opensearch=true ``` +When using OpenSearch, you might see log messages like: +``` +[o.o.w.QueryGroupTask] QueryGroup _id can't be null +``` +This is a known issue in OpenSearch 2.18 that doesn't affect functionality. You can track this issue at: +https://github.com/opensearch-project/OpenSearch/issues/16874 + ## Debugging integration tests If you want to run the tests with a debugger, you can use the `it.karaf.debug` system property. diff --git a/manual/src/main/asciidoc/configuration.adoc b/manual/src/main/asciidoc/configuration.adoc index c127b247f..0eca5c6e3 100644 --- a/manual/src/main/asciidoc/configuration.adoc +++ b/manual/src/main/asciidoc/configuration.adoc @@ -68,20 +68,46 @@ org.apache.unomi.cluster.public.address=http://localhost:8181 org.apache.unomi.cluster.internal.address=https://localhost:9443 ---- -If you need to specify an ElasticSearch cluster name, or a host and port that are different than the default, -it is recommended to do this BEFORE you start the server for the first time, or you will loose all the data +If you need to specify a search engine configuration that is different than the default, +it is recommended to do this BEFORE you start the server for the first time, or you will lose all the data you have stored previously. -You can use the following properties for the ElasticSearch configuration +Apache Unomi supports both ElasticSearch and OpenSearch as search engine backends. Here are the configuration properties for each: + +For ElasticSearch: [source] ---- org.apache.unomi.elasticsearch.cluster.name=contextElasticSearch -# The elasticsearch.adresses may be a comma seperated list of host names and ports such as +# The elasticsearch.addresses may be a comma separated list of host names and ports such as # hostA:9200,hostB:9200 # Note: the port number must be repeated for each host. org.apache.unomi.elasticsearch.addresses=localhost:9200 ---- +For OpenSearch: +[source] +---- +org.apache.unomi.opensearch.cluster.name=opensearch-cluster +# The opensearch.addresses may be a comma separated list of host names and ports such as +# hostA:9200,hostB:9200 +# Note: the port number must be repeated for each host. +org.apache.unomi.opensearch.addresses=localhost:9200 + +# OpenSearch security settings (if needed) +org.apache.unomi.opensearch.ssl.enable=false +org.apache.unomi.opensearch.username= +org.apache.unomi.opensearch.password= +---- + +To select which search engine to use, you can: +1. Use the appropriate configuration properties above +2. When building from source, use the appropriate Maven profile: + * For ElasticSearch (default): no special profile needed + * For OpenSearch: add `-P opensearch` to your Maven command + +Note: When using OpenSearch 2.18, you might see log messages about "QueryGroup _id can't be null". This is a known issue +that doesn't affect functionality and is being tracked at: https://github.com/opensearch-project/OpenSearch/issues/16874 + === Secured events configuration Apache Unomi secures some events by default. It comes out of the box with a default configuration that you can adjust diff --git a/package/pom.xml b/package/pom.xml index 40e9bad13..fbdb8742c 100644 --- a/package/pom.xml +++ b/package/pom.xml @@ -347,10 +347,6 @@ <startupBundles> <bundle>mvn:org.apache.unomi/log4j-extension/${project.version}</bundle> </startupBundles> - <installedFeatures> - <feature>wrapper</feature> - <feature>cxf-commands</feature> - </installedFeatures> <startupFeatures> <feature>eventadmin</feature> </startupFeatures> @@ -379,6 +375,19 @@ <feature>unomi-base</feature> <feature>unomi-startup</feature> </bootFeatures> + <installedFeatures> + <feature>wrapper</feature> + <feature>cxf-commands</feature> + <feature>unomi-persistence-elasticsearch</feature> + <feature>unomi-persistence-opensearch</feature> + <feature>unomi-services</feature> + <feature>unomi-router-karaf-feature</feature> + <feature>unomi-groovy-actions</feature> + <feature>unomi-web-applications</feature> + <feature>unomi-rest-ui</feature> + <feature>unomi-healthcheck</feature> + <feature>cdp-graphql-feature</feature> + </installedFeatures> <javase>1.8</javase> </configuration> </plugin> diff --git a/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/service/MigrationConfig.java b/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/service/MigrationConfig.java index f75929642..9dfe7a9ff 100644 --- a/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/service/MigrationConfig.java +++ b/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/service/MigrationConfig.java @@ -18,6 +18,7 @@ package org.apache.unomi.shell.migration.service; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; import org.osgi.service.component.annotations.Modified; import java.util.Collections; @@ -27,7 +28,7 @@ import java.util.Map; /** * Service uses to provide configuration information for the migration */ -@Component(immediate = true, service = MigrationConfig.class, configurationPid = {"org.apache.unomi.migration"}) +@Component(immediate = true, service = MigrationConfig.class, configurationPid = {"org.apache.unomi.migration"}, configurationPolicy = ConfigurationPolicy.REQUIRE) public class MigrationConfig { public static final String CONFIG_ES_ADDRESS = "esAddress"; diff --git a/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/UnomiManagementService.java b/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/UnomiManagementService.java index 5258cafa8..733bdaa47 100644 --- a/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/UnomiManagementService.java +++ b/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/UnomiManagementService.java @@ -30,11 +30,27 @@ public interface UnomiManagementService { * @param mustStartFeatures true if features should be started, false if they should not * @throws BundleException if there was an error starting Unomi's bundles */ - void startUnomi(String selectedPersistenceImplementation, boolean mustStartFeatures) throws BundleException; + void startUnomi(String selectedPersistenceImplementation, boolean mustStartFeatures) throws Exception; + + /** + * This method will start Apache Unomi with the specified persistence implementation + * @param selectedPersistenceImplementation the persistence implementation to use + * @param mustStartFeatures true if features should be started, false if they should not + * @param waitForCompletion true if the method should wait for completion, false if it should not + * @throws BundleException if there was an error starting Unomi's bundles + */ + void startUnomi(String selectedPersistenceImplementation, boolean mustStartFeatures, boolean waitForCompletion) throws Exception; + + /** + * This method will stop Apache Unomi + * @throws BundleException if there was an error stopping Unomi's bundles + */ + void stopUnomi() throws Exception; /** * This method will stop Apache Unomi + * @param waitForCompletion true if the method should wait for completion, false if it should not * @throws BundleException if there was an error stopping Unomi's bundles */ - void stopUnomi() throws BundleException; + void stopUnomi(boolean waitForCompletion) throws Exception; } diff --git a/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/internal/UnomiManagementServiceImpl.java b/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/internal/UnomiManagementServiceImpl.java index da3c251d7..76a7da4a3 100644 --- a/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/internal/UnomiManagementServiceImpl.java +++ b/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/internal/UnomiManagementServiceImpl.java @@ -24,17 +24,14 @@ import org.apache.unomi.lifecycle.BundleWatcher; import org.apache.unomi.shell.migration.MigrationService; import org.apache.unomi.shell.services.UnomiManagementService; import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleException; import org.osgi.service.component.ComponentContext; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.*; import org.osgi.service.metatype.annotations.Designate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +import java.util.concurrent.*; /** * Implementation of the {@link UnomiManagementService} interface, providing functionality to manage @@ -84,11 +81,14 @@ import java.util.*; * @see org.apache.karaf.features.FeaturesService * @see org.apache.karaf.features.Feature */ -@Component(service = UnomiManagementService.class, immediate = true, configurationPid = "org.apache.unomi.start") +@Component(service = UnomiManagementService.class, immediate = true, configurationPid = "org.apache.unomi.start", configurationPolicy = ConfigurationPolicy.REQUIRE) @Designate(ocd = UnomiManagementServiceConfiguration.class) public class UnomiManagementServiceImpl implements UnomiManagementService { private static final Logger LOGGER = LoggerFactory.getLogger(UnomiManagementServiceImpl.class.getName()); + private static final int DEFAULT_TIMEOUT = 300; // 5 minutes timeout + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); private static final String CDP_GRAPHQL_FEATURE = "cdp-graphql-feature"; @@ -107,10 +107,9 @@ public class UnomiManagementServiceImpl implements UnomiManagementService { private final List<String> installedFeatures = new ArrayList<>(); private final List<String> startedFeatures = new ArrayList<>(); - private String selectedPersistenceImplementation = "elasticsearch"; - @Activate public void init(ComponentContext componentContext, UnomiManagementServiceConfiguration config) throws Exception { + LOGGER.info("Initializing Unomi management service with configuration {}", config); try { this.bundleContext = componentContext.getBundleContext(); this.startFeatures = parseStartFeatures(config.startFeatures()); @@ -119,13 +118,23 @@ public class UnomiManagementServiceImpl implements UnomiManagementService { migrationService.migrateUnomi(bundleContext.getProperty("unomi.autoMigrate"), true, null); } - if (StringUtils.isNotBlank(bundleContext.getProperty("unomi.autoStart")) && - bundleContext.getProperty("unomi.autoStart").equals("true")) { - startUnomi(selectedPersistenceImplementation, true); + String autoStart = bundleContext.getProperty("unomi.autoStart"); + if (StringUtils.isNotBlank(autoStart)) { + String resolvedAutoStart = autoStart; + if ("true".equals(autoStart)) { + resolvedAutoStart = "elasticsearch"; + } if ("elasticsearch".equals(autoStart)) { + resolvedAutoStart = "elasticsearch"; + } if ("opensearch".equals(autoStart)) { + resolvedAutoStart = "opensearch"; + } + LOGGER.info("Auto-starting unomi management service with {}", resolvedAutoStart); + // Don't wait for completion during initialization + startUnomi(resolvedAutoStart, true, false); } - } catch (Exception e) { - LOGGER.error("Error during Unomi startup when processing 'unomi.autoMigrate' or 'unomi.autoStart' properties:", e); + LOGGER.error("Error during Unomi startup:", e); + throw e; } } @@ -159,10 +168,33 @@ public class UnomiManagementServiceImpl implements UnomiManagementService { } @Override - public void startUnomi(String selectedPersistenceImplementation, boolean mustStartFeatures) throws BundleException { - if (selectedPersistenceImplementation != null) { - this.selectedPersistenceImplementation = selectedPersistenceImplementation; + public void startUnomi(String selectedPersistenceImplementation, boolean mustStartFeatures) throws Exception { + // Default to waiting for completion + startUnomi(selectedPersistenceImplementation, mustStartFeatures, true); + } + + @Override + public void startUnomi(String selectedPersistenceImplementation, boolean mustStartFeatures, boolean waitForCompletion) throws Exception { + Future<?> future = executor.submit(() -> { + try { + doStartUnomi(selectedPersistenceImplementation, mustStartFeatures); + } catch (Exception e) { + LOGGER.error("Error starting Unomi:", e); + throw new RuntimeException(e); + } + }); + + if (waitForCompletion) { + try { + future.get(DEFAULT_TIMEOUT, TimeUnit.SECONDS); + } catch (TimeoutException e) { + LOGGER.error("Timeout waiting for Unomi to start", e); + throw e; + } } + } + + private void doStartUnomi(String selectedPersistenceImplementation, boolean mustStartFeatures) throws Exception { List<String> features = startFeatures.get(selectedPersistenceImplementation); if (features == null || features.isEmpty()) { LOGGER.warn("No features configured for persistence implementation: {}", selectedPersistenceImplementation); @@ -211,7 +243,33 @@ public class UnomiManagementServiceImpl implements UnomiManagementService { } @Override - public void stopUnomi() throws BundleException { + public void stopUnomi() throws Exception { + // Default to waiting for completion + stopUnomi(true); + } + + @Override + public void stopUnomi(boolean waitForCompletion) throws Exception { + Future<?> future = executor.submit(() -> { + try { + doStopUnomi(); + } catch (Exception e) { + LOGGER.error("Error stopping Unomi:", e); + throw new RuntimeException(e); + } + }); + + if (waitForCompletion) { + try { + future.get(DEFAULT_TIMEOUT, TimeUnit.SECONDS); + } catch (TimeoutException e) { + LOGGER.error("Timeout waiting for Unomi to stop", e); + throw e; + } + } + } + + private void doStopUnomi() throws Exception { if (startedFeatures.isEmpty()) { LOGGER.info("No features to stop."); } else { @@ -265,4 +323,17 @@ public class UnomiManagementServiceImpl implements UnomiManagementService { featuresService.updateFeaturesState(stateChanges, EnumSet.of(FeaturesService.Option.Verbose)); } + @Deactivate + public void deactivate() { + executor.shutdown(); + try { + if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + }