From f9e1f82cf2c04816da06ed3a766abf57487c22cb Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 06 Jul 2026 06:52:41 +0000
Subject: [PATCH] Modernizes the OpenDJ Docker images and broadens their multi-architecture build matrix. (#655)

---
 .github/benchmark/compare-opendj.sh             |  110 +++++++++++++++++++++++++++
 opendj-packages/opendj-docker/Dockerfile-alpine |    5 +
 .github/workflows/release.yml                   |    4 
 .github/workflows/build.yml                     |   58 ++++++++++++++
 .github/benchmark/summary.sh                    |    4 
 opendj-packages/opendj-docker/Dockerfile        |    2 
 6 files changed, 175 insertions(+), 8 deletions(-)

diff --git a/.github/benchmark/compare-opendj.sh b/.github/benchmark/compare-opendj.sh
new file mode 100644
index 0000000..1825921
--- /dev/null
+++ b/.github/benchmark/compare-opendj.sh
@@ -0,0 +1,110 @@
+#!/usr/bin/env bash
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions copyright [year] [name of copyright owner]".
+#
+# Copyright 2026 3A Systems, LLC.
+#
+# Compare two OpenDJ Docker images with the LDAP benchmark (.github/benchmark/benchmark.jmx) and
+# append the comparison report to $GITHUB_STEP_SUMMARY. Both sides are OpenDJ, so no per-server
+# hashing/index setup is needed (identical product => identical default password scheme).
+#
+# Usage:
+#   compare-opendj.sh <A_name> <A_image> <B_name> <B_image>
+# Env: THREADS (default 200), DURATION (default 150), JMETER_VERSION (default 5.6.3).
+set -euo pipefail
+
+A_NAME="${1:?server A name required}"
+A_IMAGE="${2:?server A image required}"
+B_NAME="${3:?server B name required}"
+B_IMAGE="${4:?server B image required}"
+
+THREADS="${THREADS:-200}"
+DURATION="${DURATION:-150}"
+JMETER_VERSION="${JMETER_VERSION:-5.6.3}"
+BASEDN="dc=example,dc=com"
+BENCHPW="benchPass1"
+HERE="$(cd "$(dirname "$0")" && pwd)"   # .github/benchmark
+
+# ---------------------------------------------------------------- dependencies
+if ! command -v ldapsearch >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
+  sudo apt-get update -qq
+  sudo apt-get install -y -qq ldap-utils jq
+fi
+JM="$HOME/jmeter/apache-jmeter-$JMETER_VERSION/bin/jmeter"
+if [ ! -x "$JM" ]; then
+  mkdir -p "$HOME/jmeter"
+  curl -fsSL "https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-$JMETER_VERSION.tgz" -o /tmp/jmeter.tgz
+  tar -xzf /tmp/jmeter.tgz -C "$HOME/jmeter"
+fi
+
+wait_dj() {  # poll OpenDJ readiness on localhost:1389
+  for _ in $(seq 1 90); do
+    ldapsearch -x -H ldap://localhost:1389 -D "cn=Directory Manager" -w password \
+      -b "$BASEDN" -s base dn >/dev/null 2>&1 && return 0
+    sleep 2
+  done
+  return 1
+}
+
+# bench_one <image> <out-slug>  -> prints the captured server version (stdout only)
+bench_one() {
+  local image="$1" out="$2" ver=""
+  docker rm -f opendj-bench >/dev/null 2>&1 || true
+  if docker run -d --name opendj-bench -p 1389:1389 \
+       -e ROOT_PASSWORD=password -e BASE_DN="$BASEDN" -e ADD_BASE_ENTRY=--addBaseEntry \
+       "$image" >/dev/null 2>&1; then
+    wait_dj || echo "WARN: $image not ready in time" >&2
+    ldapadd -x -H ldap://localhost:1389 -D "cn=Directory Manager" -w password \
+      -f "$HERE/people.ldif" >/dev/null 2>&1 || true
+    ver="$( { ldapsearch -x -LLL -H ldap://localhost:1389 -D 'cn=Directory Manager' -w password \
+              -b '' -s base fullVendorVersion 2>/dev/null || true; } | sed -n 's/^fullVendorVersion: //p')"
+    rm -rf "$out" "$out.jtl"
+    HEAP="-Xms1g -Xmx2g" "$JM" -n -t "$HERE/benchmark.jmx" \
+      -Jhost=localhost -Jport=1389 -Jbasedn="$BASEDN" \
+      -Jadminbinddn="cn=Directory Manager" -Jadminbindpw=password -Jbenchpw="$BENCHPW" \
+      -Jthreads="$THREADS" -Jduration="$DURATION" -Jrampup=0 \
+      -Jjmeter.reportgenerator.sample_filter='^(?!ADMIN_CONNECT).*' \
+      -l "$out.jtl" -e -o "$out" > "$out.jmeter.out" 2>&1 || true
+    docker logs opendj-bench > "$out.docker.log" 2>&1 || true
+    # surface distinct error messages to the step log (stderr; stdout carries the version)
+    if [ -f "$out.jtl" ]; then
+      local errs
+      errs="$(awk -F',' 'NR==1{for(i=1;i<=NF;i++)h[$i]=i; next}
+                         tolower($h["success"])=="false"{print $h["label"]" | "$h["responseCode"]" | "$h["responseMessage"]}' \
+              "$out.jtl" 2>/dev/null | sort | uniq -c | sort -rn | head -10)"
+      [ -z "$errs" ] || { echo "[$out] errors (count | op | code | message):" >&2; echo "$errs" >&2; }
+    fi
+  else
+    echo "ERROR: failed to start image $image" >&2
+  fi
+  docker rm -f opendj-bench >/dev/null 2>&1 || true
+  [ -n "$ver" ] || ver="$image"
+  printf '%s' "$ver"
+}
+
+echo "Benchmarking ${A_NAME} (${A_IMAGE}) @ ${THREADS} threads / ${DURATION}s ..."
+A_VER="$(bench_one "$A_IMAGE" a)"
+echo "Benchmarking ${B_NAME} (${B_IMAGE}) @ ${THREADS} threads / ${DURATION}s ..."
+B_VER="$(bench_one "$B_IMAGE" b)"
+
+{
+  bash "$HERE/summary.sh" \
+    "$A_NAME" a/statistics.json "$A_VER" "$A_IMAGE" \
+    "$B_NAME" b/statistics.json "$B_VER" "$B_IMAGE"
+  echo ""
+  echo "### Notes"
+  echo ""
+  echo "- **${A_NAME}** = freshly built image; **${B_NAME}** = latest released image. Both are"
+  echo "  OpenDJ, so they share the same default password storage scheme (hashing parity is automatic)."
+  echo "- The admin connection bind (\`ADMIN_CONNECT\`) is cached per thread and excluded; \`BIND\` is"
+  echo "  the measured user authentication (\`test=sbind\`, single bind/unbind)."
+} >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"
diff --git a/.github/benchmark/summary.sh b/.github/benchmark/summary.sh
index b2b59f9..9ea5ce4 100644
--- a/.github/benchmark/summary.sh
+++ b/.github/benchmark/summary.sh
@@ -115,8 +115,8 @@
 echo "_Per-operation throughput is not charted: every op runs once per loop iteration, so each"
 echo "op's throughput just equals the loop rate. The meaningful throughput is the aggregate._"
 echo ""
-TP_CFG="{\"type\":\"horizontalBar\",\"data\":{\"labels\":[\"${A_NAME}\",\"${B_NAME}\"],\"datasets\":[{\"label\":\"tests/s\",\"backgroundColor\":[\"$A_COLOR\",\"$B_COLOR\"],\"data\":[${a_tot_tp},${b_tot_tp}]}]},\"options\":{\"legend\":{\"display\":false},\"title\":{\"display\":true,\"text\":\"Total throughput (tests/s)\"}}}"
-echo "![Total throughput (tests/s)]($(qc 700 280 "$TP_CFG"))"
+TP_CFG="{\"type\":\"bar\",\"data\":{\"labels\":[\"${A_NAME}\",\"${B_NAME}\"],\"datasets\":[{\"label\":\"tests/s\",\"backgroundColor\":[\"$A_COLOR\",\"$B_COLOR\"],\"data\":[${a_tot_tp},${b_tot_tp}]}]},\"options\":{\"legend\":{\"display\":false},\"title\":{\"display\":true,\"text\":\"Total throughput (tests/s)\"}}}"
+echo "![Total throughput (tests/s)]($(qc 500 320 "$TP_CFG"))"
 echo ""
 
 # ---------------------------------------------------------------- Latency chart (grouped bars)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 6932ab1..2503bd4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -315,6 +315,9 @@
         ports:
           - 5000:5000
     steps:
+      - uses: actions/checkout@v6
+        with:
+          sparse-checkout: .github/benchmark
       - name: Download artifacts
         uses: actions/download-artifact@v8
         with:
@@ -351,7 +354,7 @@
           file: ./opendj-packages/opendj-docker/Dockerfile
           build-args: |
             VERSION=${{ env.release_version }}
-          platforms: linux/amd64, linux/arm64/8, linux/ppc64le, linux/s390x #, linux/arm/v7
+          platforms: linux/amd64, linux/arm64/8, linux/ppc64le, linux/s390x, linux/riscv64 #, linux/arm/v7
           push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
@@ -376,6 +379,30 @@
           timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_custom | grep -q \"healthy\"; do sleep 10; done'
           docker exec test_custom 'sh' '-c' '/opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword custom_password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1'
           docker kill test_custom
+      - name: Cache JMeter
+        uses: actions/cache@v5
+        with:
+          path: ~/jmeter
+          key: jmeter-5.6.3
+      - name: Benchmark Build vs Release
+        shell: bash
+        run: |
+          THREADS=200 DURATION=150 bash .github/benchmark/compare-opendj.sh \
+            "Build"   localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }} \
+            "Release" openidentityplatform/opendj:latest
+      - name: Upload benchmark artifacts
+        if: always()
+        uses: actions/upload-artifact@v7
+        with:
+          name: benchmark-build-vs-release
+          path: |
+            a/
+            b/
+            *.jtl
+            *.docker.log
+            *.jmeter.out
+          if-no-files-found: warn
+          retention-days: 3
 
   build-docker-alpine:
     needs: build-maven
@@ -386,6 +413,9 @@
         ports:
           - 5000:5000
     steps:
+      - uses: actions/checkout@v6
+        with:
+          sparse-checkout: .github/benchmark
       - name: Download artifacts
         uses: actions/download-artifact@v8
         with:
@@ -423,7 +453,7 @@
           file: ./opendj-packages/opendj-docker/Dockerfile-alpine
           build-args: |
             VERSION=${{ env.release_version }}
-          platforms: linux/amd64, linux/arm64/8, linux/s390x, linux/ppc64le
+          platforms: linux/amd64, linux/arm64/8, linux/s390x, linux/ppc64le, linux/386, linux/riscv64
           push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
@@ -448,3 +478,27 @@
           timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_custom | grep -q \"healthy\"; do sleep 10; done'
           docker exec test_custom 'sh' '-c' '/opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword custom_password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1'
           docker kill test_custom
+      - name: Cache JMeter
+        uses: actions/cache@v5
+        with:
+          path: ~/jmeter
+          key: jmeter-5.6.3
+      - name: Benchmark Build-alpine vs Release-alpine
+        shell: bash
+        run: |
+          THREADS=200 DURATION=150 bash .github/benchmark/compare-opendj.sh \
+            "Build-alpine"   localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}-alpine \
+            "Release-alpine" openidentityplatform/opendj:alpine
+      - name: Upload benchmark artifacts
+        if: always()
+        uses: actions/upload-artifact@v7
+        with:
+          name: benchmark-build-vs-release-alpine
+          path: |
+            a/
+            b/
+            *.jtl
+            *.docker.log
+            *.jmeter.out
+          if-no-files-found: warn
+          retention-days: 90
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 41bb3bc..1309c07 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -174,7 +174,7 @@
           file: ./opendj-packages/opendj-docker/Dockerfile
           build-args: |
             VERSION=${{ github.event.inputs.releaseVersion }}
-          platforms: linux/amd64, linux/arm64/8, linux/ppc64le, linux/s390x #, linux/arm/v7
+          platforms: linux/amd64, linux/arm64/8, linux/ppc64le, linux/s390x, linux/riscv64 #, linux/arm/v7
           push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
@@ -222,7 +222,7 @@
           file: ./opendj-packages/opendj-docker/Dockerfile-alpine
           build-args: |
             VERSION=${{ github.event.inputs.releaseVersion }}
-          platforms: linux/amd64, linux/arm64/8, linux/s390x, linux/ppc64le
+          platforms: linux/amd64, linux/arm64/8, linux/s390x, linux/ppc64le, linux/386, linux/riscv64
           push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
diff --git a/opendj-packages/opendj-docker/Dockerfile b/opendj-packages/opendj-docker/Dockerfile
index 4aa8ba5..0a9c918 100644
--- a/opendj-packages/opendj-docker/Dockerfile
+++ b/opendj-packages/opendj-docker/Dockerfile
@@ -12,7 +12,7 @@
 #
 # Copyright 2018-2026 3A Systems, LLC.
 
-FROM eclipse-temurin:25-jre-jammy
+FROM eclipse-temurin:25-jre-noble
 
 LABEL org.opencontainers.image.authors="Open Identity Platform Community"
 
diff --git a/opendj-packages/opendj-docker/Dockerfile-alpine b/opendj-packages/opendj-docker/Dockerfile-alpine
index 5794b5d..65f1f0b 100644
--- a/opendj-packages/opendj-docker/Dockerfile-alpine
+++ b/opendj-packages/opendj-docker/Dockerfile-alpine
@@ -38,13 +38,16 @@
 
 ARG VERSION
 
+ARG TARGETARCH
+
 WORKDIR /opt
 
 #COPY opendj-*.zip $OPENDJ_DIST_FILENAME
 
 RUN apk add --update --no-cache --virtual builddeps curl unzip \
  && apk upgrade --update --no-cache \
- && apk add bash openjdk11 \
+ && if [ "$TARGETARCH" = "386" ]; then JDK=openjdk11-jre; else JDK=openjdk25-jre; fi \
+ && apk add bash "$JDK" \
  && if [ -z "$VERSION" ] ; then VERSION="$(curl -i -o - --silent https://api.github.com/repos/OpenIdentityPlatform/OpenDJ/releases/latest | grep -m1 "\"name\"" | cut -d\" -f4)"; fi \
  && if [ ! -f "$OPENDJ_DIST_FILENAME" ]; then echo file exists && curl -L https://github.com/OpenIdentityPlatform/OpenDJ/releases/download/$VERSION/opendj-$VERSION.zip --output $OPENDJ_DIST_FILENAME; fi \
  && unzip $OPENDJ_DIST_FILENAME \

--
Gitblit v1.10.0