diff --git a/opendj-packages/opendj-docker/Dockerfile b/opendj-packages/opendj-docker/Dockerfile index ab4a45b279..9181389317 100644 --- a/opendj-packages/opendj-docker/Dockerfile +++ b/opendj-packages/opendj-docker/Dockerfile @@ -33,6 +33,8 @@ ENV OPENDJ_USER="opendj" ENV BACKEND_TYPE="je" ENV BACKEND_DB_DIRECTORY="db" #ENV SETUP_ARGS +# written by run.sh once the bootstrap has succeeded, read by the health check below +ENV BOOTSTRAP_COMPLETE="/opt/opendj/.bootstrap-complete" ARG OPENDJ_DIST_FILENAME=opendj.zip @@ -66,6 +68,13 @@ EXPOSE $PORT/tcp $LDAPS_PORT/tcp $ADMIN_PORT/tcp USER $OPENDJ_USER -HEALTHCHECK --interval=30s --timeout=30s --start-period=1s --retries=3 CMD opendj/bin/ldapsearch --hostname localhost --port $LDAPS_PORT --bindDN "$ROOT_USER_DN" --bindPassword "${ROOT_PASSWORD:-password}" --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1 +# "healthy" has to mean the instance is ready to serve, not just that it answers: setup +# starts the server in the middle of the bootstrap, before the backend of BASE_DN is +# created and its entries imported, so probing the root DSE alone reports ready while a +# search of BASE_DN still fails with "No Such Entry". Testing the marker first also keeps +# the probe from launching a JVM every interval until the bootstrap is through. The start +# period is what a bootstrap importing SAMPLE_DATA into a small container can take; a +# probe that succeeds ends it early, and a bootstrap that failed never writes the marker. +HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 CMD test -f "$BOOTSTRAP_COMPLETE" && opendj/bin/ldapsearch --hostname localhost --port $LDAPS_PORT --bindDN "$ROOT_USER_DN" --bindPassword "${ROOT_PASSWORD:-password}" --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1 ENTRYPOINT ["/opt/opendj/run.sh"] diff --git a/opendj-packages/opendj-docker/Dockerfile-alpine b/opendj-packages/opendj-docker/Dockerfile-alpine index 65f1f0bb34..5f2635faae 100644 --- a/opendj-packages/opendj-docker/Dockerfile-alpine +++ b/opendj-packages/opendj-docker/Dockerfile-alpine @@ -33,6 +33,8 @@ ENV OPENDJ_USER="opendj" ENV BACKEND_TYPE="je" ENV BACKEND_DB_DIRECTORY="db" #ENV SETUP_ARGS +# written by run.sh once the bootstrap has succeeded, read by the health check below +ENV BOOTSTRAP_COMPLETE="/opt/opendj/.bootstrap-complete" ARG OPENDJ_DIST_FILENAME=opendj.zip @@ -70,6 +72,13 @@ EXPOSE $PORT/tcp $LDAPS_PORT/tcp $ADMIN_PORT/tcp USER $OPENDJ_USER -HEALTHCHECK --interval=30s --timeout=30s --start-period=1s --retries=3 CMD opendj/bin/ldapsearch --hostname localhost --port $LDAPS_PORT --bindDN "$ROOT_USER_DN" --bindPassword "${ROOT_PASSWORD:-password}" --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1 +# "healthy" has to mean the instance is ready to serve, not just that it answers: setup +# starts the server in the middle of the bootstrap, before the backend of BASE_DN is +# created and its entries imported, so probing the root DSE alone reports ready while a +# search of BASE_DN still fails with "No Such Entry". Testing the marker first also keeps +# the probe from launching a JVM every interval until the bootstrap is through. The start +# period is what a bootstrap importing SAMPLE_DATA into a small container can take; a +# probe that succeeds ends it early, and a bootstrap that failed never writes the marker. +HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 CMD test -f "$BOOTSTRAP_COMPLETE" && opendj/bin/ldapsearch --hostname localhost --port $LDAPS_PORT --bindDN "$ROOT_USER_DN" --bindPassword "${ROOT_PASSWORD:-password}" --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1 ENTRYPOINT ["/opt/opendj/run.sh"] diff --git a/opendj-packages/opendj-docker/README.md b/opendj-packages/opendj-docker/README.md index 9c40fc3459..0b85aa0471 100644 --- a/opendj-packages/opendj-docker/README.md +++ b/opendj-packages/opendj-docker/README.md @@ -12,11 +12,36 @@ Run image docker run -d -p 1389:1389 -p 1636:1636 -p 4444:4444 --name opendj openidentityplatform/opendj ``` +## Health check + +The image reports itself `healthy` once the server answers on `LDAPS_PORT` *and* the whole +bootstrap has succeeded - the instance, the `userRoot` backend over `BASE_DN`, whatever +`ADD_BASE_ENTRY` and `SAMPLE_DATA` asked to be imported into it, and the replication asked +for by `MASTER_SERVER`. Waiting for that status is therefore enough before the first search +of what the bootstrap was told to create: + +```bash +docker run -d --name opendj -e ADD_BASE_ENTRY=--addBaseEntry openidentityplatform/opendj +timeout 5m bash -c 'until [ "$(docker inspect -f "{{.State.Health.Status}}" opendj)" = healthy ]; do sleep 5; done' +``` + +In Compose the same is `depends_on: { opendj: { condition: service_healthy } }`. Note that +without `ADD_BASE_ENTRY` nothing creates the base entry, so `BASE_DN` is an empty suffix on +a healthy container - the health check itself searches the root DSE, which every instance +serves whatever it was set up to hold. + +A bootstrap that imports `SAMPLE_DATA` can take minutes on a small container, which is what +the start period allows for. A bootstrap that fails - or an upgrade that fails when starting +over an instance that is already there - never reports healthy: what failed is in `docker +logs`, and where the server is up at all the container is left running to be looked at, +turning `unhealthy` once the start period is over. + ## Environment Variables | Variable | Default Value | Description | |-------------------------|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ADD_BASE_ENTRY | | if set --addBaseEntry , creates base DN entry | +| SAMPLE_DATA | - | with ADD_BASE_ENTRY set, imports that many generated users under BASE_DN instead of the base entry alone | | PORT | 1389 | LDAP Listener Port | | LDAPS_PORT | 1636 | LDAPS Listener Port | | BASE_DN | dc=example,dc=com | OpenDJ Base DN | diff --git a/opendj-packages/opendj-docker/bootstrap/setup.sh b/opendj-packages/opendj-docker/bootstrap/setup.sh index a71782cad2..c571af0af4 100755 --- a/opendj-packages/opendj-docker/bootstrap/setup.sh +++ b/opendj-packages/opendj-docker/bootstrap/setup.sh @@ -1,5 +1,24 @@ #!/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]". +# +# Portions copyright 2026 3A Systems, LLC. + # Default setup script +# +# What the instance is made of - the server itself, the backend, the entries the backend was +# asked to hold - is left to fail this script, which is what run.sh reads to decide whether +# the container may report itself healthy. The optional schema and data LDIFs below keep the +# tolerance they were written with. echo "Setting up default OpenDJ instance" @@ -30,7 +49,7 @@ fi --acceptLicense \ --no-prompt \ --noPropertiesFile \ - $SETUP_ARGS + $SETUP_ARGS || exit 1 BACKEND_TYPE=${BACKEND_TYPE:-je} BACKEND_DB_DIRECTORY=${BACKEND_DB_DIRECTORY:-db} @@ -38,21 +57,21 @@ echo "creating backend: $BACKEND_TYPE db-directory: ${BACKEND_DB_DIRECTORY}" /opt/opendj/bin/dsconfig create-backend -h localhost -p $ADMIN_PORT --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" \ --backend-name=userRoot --type $BACKEND_TYPE --set base-dn:$BASE_DN --set "db-directory:$BACKEND_DB_DIRECTORY" \ - --set enabled:true --no-prompt --trustAll + --set enabled:true --no-prompt --trustAll || exit 1 if [ "$ADD_BASE_ENTRY" = "--addBaseEntry" ]; then BASE_TEMPLATE=$(mktemp) if [ ! -z ${SAMPLE_DATA} ]; then echo "generating sample data..." - /opt/opendj/bin/makeldif -o $BASE_TEMPLATE -c suffix="$BASE_DN" -c numusers=$SAMPLE_DATA /opt/opendj/template/config/MakeLDIF/example.template + /opt/opendj/bin/makeldif -o $BASE_TEMPLATE -c suffix="$BASE_DN" -c numusers=$SAMPLE_DATA /opt/opendj/template/config/MakeLDIF/example.template || exit 1 /opt/opendj/bin/import-ldif --ldifFile $BASE_TEMPLATE \ - --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" + --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" || exit 1 else echo "creating base entry..." BASE_TEMPLATE=$(mktemp) echo "branch: $BASE_DN" > $BASE_TEMPLATE /opt/opendj/bin/import-ldif --templateFile $BASE_TEMPLATE \ - --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" + --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" || exit 1 fi rm $BASE_TEMPLATE fi diff --git a/opendj-packages/opendj-docker/run.sh b/opendj-packages/opendj-docker/run.sh index b79e4e6dd9..9e028b2758 100755 --- a/opendj-packages/opendj-docker/run.sh +++ b/opendj-packages/opendj-docker/run.sh @@ -23,6 +23,17 @@ cd /opt/opendj +# The health check probes the server only once this marker is there, so that "healthy" +# means the instance is bootstrapped rather than merely listening: setup starts the +# server in the middle of the bootstrap, before the backend holding BASE_DN has been +# created. Nothing below writes it unless the step it stands for reported success, so a +# bootstrap that failed leaves the container running to be looked at, but never healthy. +# It is kept outside ./data because it records what this container has done, not what +# the volume holds - and a restart of a container replays this script over the writable +# layer the previous run left behind, so it is cleared before anything else. +BOOTSTRAP_COMPLETE=${BOOTSTRAP_COMPLETE:-/opt/opendj/.bootstrap-complete} +rm -f "$BOOTSTRAP_COMPLETE" + #if default data folder exists do not change it if [ ! -d ./db ]; then echo "/opt/opendj/data" >/opt/opendj/instance.loc && \ @@ -31,7 +42,13 @@ fi # Instance dir does exist? We start opendj without detach if [ -d ./data/config ]; then - sh ./upgrade -n + # nothing is bootstrapped here, the instance is already there - but a half-migrated one + # is not ready to serve either, so the marker follows the upgrade + if sh ./upgrade -n; then + touch "$BOOTSTRAP_COMPLETE" + else + echo "Upgrade failed, this container will not report itself healthy" + fi exec ./bin/start-ds --nodetach exit fi @@ -46,11 +63,18 @@ export ROOT_PASSWORD=${ROOT_PASSWORD:-password} BOOTSTRAP=${BOOTSTRAP:-/opt/opendj/bootstrap/setup.sh} echo "Running $BOOTSTRAP" -sh "${BOOTSTRAP}" +BOOTSTRAPPED=true +if ! sh "${BOOTSTRAP}"; then + BOOTSTRAPPED=false + echo "$BOOTSTRAP failed, this container will not report itself healthy" +fi # Check if OPENDJ_REPLICATION_TYPE var is set. If it is - replicate to that server if [ -n "${MASTER_SERVER}" ] && [ -n "${OPENDJ_REPLICATION_TYPE}" ]; then - /opt/opendj/bootstrap/replicate.sh + if ! /opt/opendj/bootstrap/replicate.sh; then + BOOTSTRAPPED=false + echo "Replication setup failed, this container will not report itself healthy" + fi fi # Check if keystores are mounted as a volume, and if so @@ -63,6 +87,12 @@ if [ -d "${SECRET_VOLUME}" ]; then cp -f ${SECRET_VOLUME}/key* ${SECRET_VOLUME}/trust* ./data/config 2>/dev/null fi +# Everything the instance was asked to be set up with - its backend, its base entry, its +# replication - is in place from here on, so the health check may start probing the server +if [ "$BOOTSTRAPPED" = true ]; then + touch "$BOOTSTRAP_COMPLETE" +fi + # Opendj is probably already started in detach mode at the install if (bin/status -n | grep Started); then echo "OpenDJ is started"