# 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.
|
name: "Benchmark"
|
|
# Benchmark: OpenDJ vs OpenLDAP.
|
# Runs manually (workflow_dispatch) and automatically after the "Release" workflow completes.
|
# Starts the latest OpenLDAP and OpenDJ from Docker (image tags overridable via inputs),
|
# benchmarks OpenLDAP first then OpenDJ with Apache JMeter, and writes versions + comparison
|
# table + comparative charts to the job summary. Full JMeter HTML dashboards are uploaded as
|
# the "jmeter-reports" artifact.
|
|
on:
|
workflow_dispatch:
|
inputs:
|
openldap_image:
|
description: "OpenLDAP Docker image"
|
default: "osixia/openldap:latest"
|
opendj_image:
|
description: "OpenDJ Docker image"
|
default: "openidentityplatform/opendj:latest"
|
threads:
|
description: "Concurrent JMeter threads"
|
default: "256"
|
duration:
|
description: "Test duration per server (seconds)"
|
default: "600"
|
rampup:
|
description: "Ramp-up period (seconds)"
|
default: "0"
|
jmeter_version:
|
description: "Apache JMeter version"
|
default: "5.6.3"
|
workflow_run:
|
workflows: ["Release"]
|
types: [completed]
|
|
permissions:
|
contents: read
|
|
concurrency:
|
group: benchmark-${{ github.ref }}
|
cancel-in-progress: true
|
|
defaults:
|
run:
|
shell: bash
|
|
jobs:
|
benchmark:
|
# Manual runs always proceed; chained runs only after a successful Release.
|
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
|
runs-on: ubuntu-latest
|
env:
|
# `${{ inputs.X || 'default' }}` so workflow_run (which carries no inputs) falls back.
|
OPENLDAP_IMAGE: ${{ inputs.openldap_image || 'osixia/openldap:latest' }}
|
OPENDJ_IMAGE: ${{ inputs.opendj_image || 'openidentityplatform/opendj:latest' }}
|
THREADS: ${{ inputs.threads || '256' }}
|
DURATION: ${{ inputs.duration || '600' }}
|
RAMPUP: ${{ inputs.rampup || '0' }}
|
JMETER: ${{ inputs.jmeter_version || '5.6.3' }}
|
BENCHPW: benchPass1
|
BASEDN: dc=example,dc=com
|
# Exclude the cached admin connection bind from the dashboard / statistics.json.
|
SAMPLE_FILTER: '^(?!ADMIN_CONNECT).*'
|
HEAP: -Xms1g -Xmx2g
|
steps:
|
- uses: actions/checkout@v6
|
|
- name: Cache JMeter
|
uses: actions/cache@v5
|
with:
|
path: ~/jmeter
|
key: jmeter-${{ env.JMETER }}
|
|
- name: Install tooling (ldap-utils + JMeter)
|
run: |
|
sudo apt-get update
|
sudo apt-get install -y ldap-utils
|
if [ ! -x "$HOME/jmeter/apache-jmeter-$JMETER/bin/jmeter" ]; then
|
mkdir -p "$HOME/jmeter"
|
curl -fsSL "https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-$JMETER.tgz" -o /tmp/jmeter.tgz
|
tar -xzf /tmp/jmeter.tgz -C "$HOME/jmeter"
|
fi
|
echo "JMETER_BIN=$HOME/jmeter/apache-jmeter-$JMETER/bin/jmeter" >> "$GITHUB_ENV"
|
|
- name: Start OpenLDAP
|
run: |
|
docker run -d --name openldap -p 2389:389 \
|
-e LDAP_ORGANISATION="Example" \
|
-e LDAP_DOMAIN="example.com" \
|
-e LDAP_ADMIN_PASSWORD="password" \
|
-e LDAP_TLS=false \
|
"$OPENLDAP_IMAGE"
|
|
- name: Configure OpenLDAP (SSHA-256 hash-on-write, seed)
|
run: |
|
wait_ldap() {
|
for i in $(seq 1 90); do
|
ldapsearch -x -H ldap://localhost:2389 -D "cn=admin,$BASEDN" -w password \
|
-b "$BASEDN" -s base dn >/dev/null 2>&1 && { echo "OpenLDAP is up"; return 0; }
|
sleep 2
|
done
|
}
|
le() { docker exec -i openldap "$@" -Y EXTERNAL -H ldapi:/// >/dev/null 2>&1 || true; }
|
wait_ldap
|
# Load pw-sha2 (provides {SSHA256}) and ppolicy (overlay) modules; osixia ships both
|
# in /usr/lib/ldap. Create the module list entry or append the values, then restart so
|
# the modules are active in the running slapd.
|
printf 'dn: cn=module{0},cn=config\nobjectClass: olcModuleList\nolcModuleLoad: pw-sha2\nolcModuleLoad: ppolicy\n' | le ldapadd
|
printf 'dn: cn=module{0},cn=config\nchangetype: modify\nadd: olcModuleLoad\nolcModuleLoad: pw-sha2\n' | le ldapmodify
|
printf 'dn: cn=module{0},cn=config\nchangetype: modify\nadd: olcModuleLoad\nolcModuleLoad: ppolicy\n' | le ldapmodify
|
docker restart openldap
|
wait_ldap
|
# Make slapd hash cleartext userPassword with {SSHA256} on a plain modify: set the global
|
# password-hash and enable the ppolicy overlay's hash_cleartext on the mdb database.
|
printf 'dn: olcDatabase={-1}frontend,cn=config\nchangetype: modify\nreplace: olcPasswordHash\nolcPasswordHash: {SSHA256}\n' | le ldapmodify
|
DBDN="$(docker exec openldap ldapsearch -Y EXTERNAL -H ldapi:/// -LLL -b cn=config '(olcDatabase=*mdb)' dn 2>/dev/null | sed -n 's/^dn: //p' | head -1)"
|
[ -n "$DBDN" ] || DBDN="olcDatabase={1}mdb,cn=config"
|
printf 'dn: olcOverlay=ppolicy,%s\nobjectClass: olcOverlayConfig\nobjectClass: olcPPolicyConfig\nolcOverlay: ppolicy\nolcPPolicyHashCleartext: TRUE\n' "$DBDN" | le ldapadd
|
# Seed ou=People (after the restart so it survives any re-init).
|
ldapadd -x -H ldap://localhost:2389 -D "cn=admin,$BASEDN" -w password \
|
-f .github/benchmark/people.ldif || true
|
|
- name: Capture OpenLDAP version
|
run: |
|
# No `| head -1`: head closes the pipe early, slapd -VV then gets SIGPIPE and under
|
# `pipefail` fails the step with exit 141. Capture fully, take the first line in bash.
|
ver="$(docker exec openldap sh -c '/usr/sbin/slapd -VV 2>&1 || slapd -VV 2>&1' || true)"
|
ver="${ver%%$'\n'*}"
|
[ -n "$ver" ] || ver="$OPENLDAP_IMAGE"
|
echo "OpenLDAP: $ver"
|
{
|
echo "OPENLDAP_VER<<EOF"
|
echo "$ver"
|
echo "EOF"
|
} >> "$GITHUB_ENV"
|
|
- name: Benchmark OpenLDAP
|
run: |
|
rm -rf openldap openldap.jtl
|
HEAP="$HEAP" "$JMETER_BIN" -n -t .github/benchmark/benchmark.jmx \
|
-Jhost=localhost -Jport=2389 \
|
-Jbasedn="$BASEDN" \
|
-Jadminbinddn="cn=admin,$BASEDN" -Jadminbindpw=password \
|
-Jbenchpw="$BENCHPW" \
|
-Jthreads="$THREADS" -Jduration="$DURATION" -Jrampup="$RAMPUP" \
|
-Jjmeter.reportgenerator.sample_filter="$SAMPLE_FILTER" \
|
-l openldap.jtl -e -o openldap
|
|
- name: Stop OpenLDAP
|
run: |
|
docker logs openldap 2>&1 | tail -n 100 || true
|
docker rm -f openldap || true
|
|
- name: Start OpenDJ
|
run: |
|
docker run -d --name opendj -p 1389:1389 \
|
-e ROOT_PASSWORD=password \
|
-e BASE_DN="$BASEDN" \
|
-e ADD_BASE_ENTRY=--addBaseEntry \
|
"$OPENDJ_IMAGE"
|
|
- name: Wait for OpenDJ + seed ou=People
|
run: |
|
for i in $(seq 1 90); do
|
if ldapsearch -x -H ldap://localhost:1389 -D "cn=Directory Manager" -w password \
|
-b "$BASEDN" -s base dn >/dev/null 2>&1; then
|
echo "OpenDJ is up"; break
|
fi
|
sleep 2
|
done
|
ldapadd -x -H ldap://localhost:1389 -D "cn=Directory Manager" -w password \
|
-f .github/benchmark/people.ldif || true
|
|
- name: Configure OpenDJ password policy (SSHA-256 hash-on-write)
|
run: |
|
# Hash cleartext userPassword with Salted SHA-256 on write, matching OpenLDAP.
|
docker exec opendj /opt/opendj/bin/dsconfig set-password-policy-prop \
|
--policy-name "Default Password Policy" \
|
--set default-password-storage-scheme:"Salted SHA-256" \
|
--hostname localhost --port 4444 \
|
--bindDN "cn=Directory Manager" --bindPassword password \
|
--trustAll --no-prompt
|
|
- name: Capture OpenDJ version
|
run: |
|
# `|| true` guards the bind so a transient ldapsearch failure can't trip pipefail;
|
# sed reads all input (no early pipe close), so no SIGPIPE here.
|
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')"
|
[ -n "$ver" ] || ver="$OPENDJ_IMAGE"
|
echo "OpenDJ: $ver"
|
{
|
echo "OPENDJ_VER<<EOF"
|
echo "$ver"
|
echo "EOF"
|
} >> "$GITHUB_ENV"
|
|
- name: Benchmark OpenDJ
|
run: |
|
rm -rf opendj opendj.jtl
|
HEAP="$HEAP" "$JMETER_BIN" -n -t .github/benchmark/benchmark.jmx \
|
-Jhost=localhost -Jport=1389 \
|
-Jbasedn="$BASEDN" \
|
-Jadminbinddn="cn=Directory Manager" -Jadminbindpw=password \
|
-Jbenchpw="$BENCHPW" \
|
-Jthreads="$THREADS" -Jduration="$DURATION" -Jrampup="$RAMPUP" \
|
-Jjmeter.reportgenerator.sample_filter="$SAMPLE_FILTER" \
|
-l opendj.jtl -e -o opendj
|
|
- name: Stop OpenDJ
|
run: |
|
docker logs opendj 2>&1 | tail -n 100 || true
|
docker rm -f opendj || true
|
|
# ---------------------------------------------------------------- Report
|
- name: Build job summary
|
run: |
|
bash .github/benchmark/summary.sh \
|
openldap/statistics.json opendj/statistics.json \
|
"$OPENLDAP_VER" "$OPENDJ_VER" \
|
"$OPENLDAP_IMAGE" "$OPENDJ_IMAGE" >> "$GITHUB_STEP_SUMMARY"
|
|
- name: Upload JMeter reports
|
if: always()
|
uses: actions/upload-artifact@v7
|
with:
|
name: jmeter-reports
|
path: |
|
openldap/
|
opendj/
|
*.jtl
|
if-no-files-found: warn
|
|
- name: Container logs + cleanup
|
if: always()
|
run: |
|
docker logs openldap 2>&1 | tail -n 100 || true
|
docker logs opendj 2>&1 | tail -n 100 || true
|
docker rm -f openldap opendj || true
|