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();
+        }
+    }
+
 }


Reply via email to