11 KiB
AGENTS.md
Guidance for coding agents working on osixia/openldap (v2), an OpenLDAP container image built on osixia/baseimage (Alpine).
Documentation
- Keep the main
README.mdconcise and simple. For new options, add only the required environment-variable table rows there; put explanations in the relevant service README. - Document bootstrap features briefly alongside the other features in the existing bootstrap READMEs. Do not create a separate documentation page for a small feature unless explicitly requested, and do not add feature explanations to the main README.
Repository layout
| Path | Role |
|---|---|
Dockerfile |
Installs pinned Alpine openldap* packages (OPENLDAP_VERSION), copies services/ and environment/, runs as user ldap (uid/gid 911). |
environment/.env* |
Default env vars, loaded by the baseimage. One file per concern: core, bootstrap, ctl, cron, upgrade. |
services/<name>/ |
baseimage services: install.sh (image build), startup.sh (each start), process.sh (long-running process). |
services/openldap/ |
Runs slapd. Ships custom schemas, custom modules, certs dir and the openldap-ctl CLI. |
services/openldap-bootstrap/ |
First-start initialization from LDIF fragments + feature scripts. |
services/openldap-upgrade/ |
Version check + restore-based migration on start. |
services/openldap-pre-upgrade/ |
Pre-upgrade backup, meant to run as an init container on the N-1 image (see below). |
services/openldap-cron/ |
crond running scheduled backups (as root, separate container). |
build/ |
Go CI/CD tool wrapping osixia/dagger-build. Not part of the image. |
docs/examples/ |
Compose examples (quickstart, replication). |
.dockerignore excludes *.md, .gitkeep, build/, docs/: none of them reach the image.
baseimage service model
- Startup order follows
.priority, ascending (lower runs first):openldap(100) →openldap-bootstrap(150) →openldap-upgrade(200). Services without.prioritydefault to 500.openldap/startup.shcopies modules and schemas first, so bootstrap can read them.- Bootstrap must run before upgrade: on a fresh install, upgrade runs
slapd -T teston the freshly created config and writes the version file.
.optionalservices (openldap-cron,openldap-pre-upgrade) are not started by default. Enable them explicitly, e.g.osixia/openldap -x openldap-cron.- Use the baseimage CLI, not raw equivalents:
container log <level>(fatalexits),container envsubst,container processes start|stop openldap,container run -- <cmd>,container packages install. The running slapd pid file is/run/container/processes/openldap.pid.
Upgrade flow (important)
The upgrade is a backup on N-1 / restore on N mechanism, driven by the version file ${OPENLDAP_CONF_DIR}/.version (OPENLDAP_UPGRADE_CONF_VERSION_FILE).
-
openldap-pre-upgraderuns on the N-1 image, as an init container (e.g. KubernetesinitContainersusing the previous image tag, sharing the config, data and backup volumes) before the N container starts:- no version file → nothing to do;
- version file matches the image's
OPENLDAP_VERSION(atOPENLDAP_UPGRADE_MIGRATION_LEVELgranularity) →openldap-ctl backup openldap-upgrade-<conf version>(slapcatof db 0 and db 1); - otherwise → skip.
It must run on N-1 because the dump has to be produced by the slapd/slapcat version that wrote the database. slapd is not running in the init container, so
slapcatreads the files offline. -
openldap-upgraderuns on the N image at every start:- no version file →
slapd -T test, write version, exit; - same comparable version → nothing to do;
- downgrade → fatal unless
OPENLDAP_UPGRADE_FORCE=true; - otherwise needs
openldap-upgrade-<old conf version>{-config.gz,-data.gz}inOPENLDAP_BACKUP_DIR(fatal if missing, unless forced), thenopenldap-ctl restore ... --force(checks the gzip files, wipes conf/data,slapadd+slapindex) and writes the new version.
- no version file →
Consequences when editing this area:
- Cross-version contract. The pre-upgrade code that actually runs is the one shipped in the previous release. Any change to
openldap-pre-upgrade/startup.sh,openldap-upgrade/assets/scripts/common.sh(extract_version,backup_files_prefix), backup file naming (OPENLDAP_UPGRADE_BACKUP_FILES_PREFIX,OPENLDAP_CTL_BACKUP_*_FILE_SUFFIX) or the version file location only takes effect one release later, andopenldap-upgradeon N must keep understanding what N-1 produced. openldap-pre-upgradesources/container/services/openldap-upgrade/assets/scripts/common.sh, so that file is shared by both sides.- Both sides must use the same
OPENLDAP_UPGRADE_MIGRATION_LEVEL(major|minor|patch, defaultminor), otherwise the backup prefix will not match. - v1 → v2 is not supported automatically. Only v2.x → v2.y.
Bootstrap
Runs only when both OPENLDAP_CONF_DIR and OPENLDAP_DATA_DIR are empty (hidden files and lost+found ignored). It never modifies an existing database. All temporary files live in one BOOTSTRAP_TMP_DIR removed by an EXIT trap.
- env phase: every
assets/scripts/NN-*.sh env <tmpfile>appendsexport VAR=...lines (viaprepare_env_file), which are then sourced. This is where passwords are generated/hashed and derived values computed (LDIF snippets, DN parts, upper-cased booleans forTRUE/FALSELDAP syntax). - ldif phase: every script
ldif <source_dir> <tmp_dir>renders*.ldif.templatewithcontainer envsubst templatesand copies.ldiffiles from its sub-directory (base/,tls/,ppolicy/, …,custom/) into one flat directory. Same output filename = later script wins (that is howcustom/overrides/removes built-in fragments). - Files are sorted with
sort -V, stripped of leading/trailing empty lines, and concatenated. Prefix grouping by tens: files in the same ten-group (60-…69-) are glued without a blank line (same LDIF entry, so they can add attributes to one entry); a group change inserts a blank line (new entry). Files without a numeric prefix are their own group. - Result is loaded with
slapadd -n 0(config) thenslapadd -n 1(data). Use plain entries, neverchangetype: modify.
Validation lives in the env phase and fails with container log fatal: OPENLDAP_BOOTSTRAP_SUFFIX must start with dc= (00-base.sh); replication needs 2 to 9 hosts, the current FQDN among them and a replication password without " (80-replication.sh; syncrepl rid is built as <current id><provider id>1, so ids must stay single-digit).
Adding a feature: create services/openldap-bootstrap/assets/scripts/NN-feature.sh guarded by OPENLDAP_BOOTSTRAP_<FEATURE>, source helpers/common.sh (and helpers/password.sh), call check_args "$@", handle env and ldif; add templates under ldif/config/<feature>/ and/or ldif/data/<feature>/; add defaults in environment/.env.bootstrap; document in README.md, ldif/README.md and scripts/README.md. Mind overlay order: syncprov must stay last (180-…).
openldap-ctl
/usr/bin/openldap-ctl → services/openldap/assets/openldap-ctl/openldap-ctl.sh, which dispatches to command/<name>.sh (adding a file there adds a command).
backup <prefix> [--config] [--data] [--clean N] [--clean-prefix P]:slapcat | gzipintoOPENLDAP_BACKUP_DIR, written to a temp file then moved (atomic), umask 077.--cleanonly removes backup files (*-config.gz,*-data.gz) older than N days, starting with P if set; the cron usesopenldap-cron-so manual and upgrade backups are kept.restore <prefix> [--config] [--data] [--force]: first checks the backup files exist and are valid gzip (before stopping or deleting anything), then refuses if slapd runs or dirs are not empty unless--force(stops/restarts slapd). Existing data is moved to<dir>/.restore-previous(same volume) and put back if the restore fails; a leftover.restore-previous(interrupted restore) blocks the next restore.password generate|hash,schema2ldif <file.schema> [deps].
Coding conventions
- Bash only. Every script starts with:
#!/bin/bash set -euo pipefail # if container log level is trace: # print commands and their arguments as they are executed container log level eq trace && set -x - Quote and brace every expansion:
"${VAR}". Use${VAR:?}beforerm -fr. - Log through
container log; pipe noisy command output with2>&1 | container log debug. Usecontainer log fatalfor errors (it exits). - Use
mktemp+trap ... RETURN/EXITfor temp files. - When
eval-ing a command taken from an env var (e.g.OPENLDAP_CTL_PASSWORD_HASH_CMD), keep user arguments inside single quotes so they are expanded after eval:eval "${CMD}"' -s "${password}"'. - Commands print a full usage line (
Usage: openldap-ctl <command> ...) and validate their arguments before doing anything destructive. - Env var names:
OPENLDAP_*, thenOPENLDAP_BOOTSTRAP_*,OPENLDAP_CTL_*,OPENLDAP_UPGRADE_*,OPENLDAP_CRON_*. New defaults go in the matchingenvironment/.env*file and the README tables. - Must pass ShellCheck (CI). Use targeted
# shellcheck disable=SCxxxxonly when needed (e.g.SC1091for sourced container paths). - Container runs as non-root
ldap: no writes outside the volumes,/container,/tmpor the dirs prepared inservices/openldap/install.sh. Default ports are 3890/6360 (unprivileged). - Comments and docs in English. Update
CHANGELOG.md(project-specific changes only) for user-visible changes.
Build, test, CI
- CI (
.github/workflows/go.yml): golangci-lint + govulncheck onbuild/, ShellCheck on the repo, then the build pipeline (multi-arch build, tests, push, cosign signing + SBOM on tags). - Integration tests live in
build/tests/(Go scenarios + bash checks intestdata/), seebuild/README.md. Add a scenario or a check there when adding a feature. Tested combinations: modules exceptdyngroup.so(conflicts withdynlist.so) andmqtt.so(conflicts withlastbind.so), schemas exceptmsuser.ldif(not loadable) andnis.ldif/virtualMail.ldif(conflict withrfc2307bis.ldif/postfix-book.ldif, tested separately). - Local:
shellcheck $(git ls-files '*.sh') cd build && go mod vendor && go run main.go job test ../Dockerfile --arches amd64 docker build -t osixia/openldap:dev . docker run --rm -it osixia/openldap:dev --log-level debug # --bash, --skip-process also useful - When bumping OpenLDAP:
OPENLDAP_VERSIONinDockerfile(Alpine package version, e.g.2.6.15-r0), base image in bothDockerfileandbuild/main.go, andCHANGELOG.md.