From bd8218b8a5c993d80a4266a4a33ccbfc7549170e Mon Sep 17 00:00:00 2001 From: Bertrand Gouny Date: Mon, 19 Oct 2026 10:11:50 +0200 Subject: [PATCH] Fix bootstrap templates on read-only volumes --- CHANGELOG.md | 5 ++ build/README.md | 2 + build/tests/bootstrap.go | 9 +++ build/tests/test.go | 1 + build/tests/testdata/templates.sh | 60 +++++++++++++++++++ .../assets/scripts/README.md | 2 +- .../assets/scripts/helpers/common.sh | 17 +++++- 7 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 build/tests/testdata/templates.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d118f..c126ca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www - Test default behavior, automatic role creation and preservation of a group after deletion of its last real member. +### Fixed + +- Render bootstrap LDIF templates in a temporary writable copy, allowing read-only + sources and Kubernetes projected-volume symlinks without modifying source files. + ## 2.6.15-beta ### Changed diff --git a/build/README.md b/build/README.md index dc5ff27..5f4d8ed 100644 --- a/build/README.md +++ b/build/README.md @@ -21,6 +21,8 @@ own native runner, then publishes them together from their OCI archives After the shared version check, `tests/` runs integration scenarios on each built image: - default bootstrap; +- read-only bootstrap templates with projected-volume symlinks, custom overrides + and temporary-copy cleanup on success and failure; - alternative schemas (`nis`, `virtualMail`); - auditlog: automatic module loading, writes, custom paths, rotation and restarts; - last successful binds: disabled by default, precision throttling, failed binds diff --git a/build/tests/bootstrap.go b/build/tests/bootstrap.go index a826bf3..217436b 100644 --- a/build/tests/bootstrap.go +++ b/build/tests/bootstrap.go @@ -15,6 +15,15 @@ const replicationPassword = `Repl passw0rd 'single' \ $HOME` const defaultSchemas = "core.ldif cosine.ldif inetorgperson.ldif rfc2307bis.ldif" +//go:embed testdata/templates.sh +var templatesCheck string + +// ReadOnlyTemplates checks projected-volume sources and temporary-copy cleanup. +func ReadOnlyTemplates(ctx context.Context, client *dagger.Client, image *dagger.Container) error { + _, err := expect(ctx, image, true, nil, "container", "run", "--", "bash", "-c", templatesCheck) + return err +} + // allSchemas can be loaded together. Excluded: // - nis.ldif and virtualMail.ldif conflict with rfc2307bis.ldif and postfix-book.ldif, // they are tested by AlternativeSchemas; diff --git a/build/tests/test.go b/build/tests/test.go index d58e323..7824c53 100644 --- a/build/tests/test.go +++ b/build/tests/test.go @@ -19,6 +19,7 @@ type scenario struct { var scenarios = []scenario{ {"default bootstrap", Default}, + {"read-only bootstrap templates", ReadOnlyTemplates}, {"alternative schemas", AlternativeSchemas}, {"audit logging", AuditLog}, {"last successful binds", LastBind}, diff --git a/build/tests/testdata/templates.sh b/build/tests/testdata/templates.sh new file mode 100644 index 0000000..16cac93 --- /dev/null +++ b/build/tests/testdata/templates.sh @@ -0,0 +1,60 @@ +#!/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 + +# shellcheck disable=SC1090 +source "${OPENLDAP_TEST_HELPER:-/container/services/openldap-bootstrap/assets/scripts/helpers/common.sh}" + +test_dir=$(mktemp -d) +trap 'chmod -R u+rwX "${test_dir:?}"; rm -rf "${test_dir:?}"' EXIT + +mkdir -p "${test_dir}/source/..revision" "${test_dir}/output" "${test_dir}/custom" +printf 'dn: %s\n' "\${OPENLDAP_TEST_DN}" > "${test_dir}/source/..revision/10-entry.ldif.template" +printf 'description: original\n' > "${test_dir}/source/..revision/20-plain.ldif" +ln -s ..revision "${test_dir}/source/..data" +ln -s ..data/10-entry.ldif.template "${test_dir}/source/10-entry.ldif.template" +ln -s ..data/20-plain.ldif "${test_dir}/source/20-plain.ldif" +chmod -R a-w "${test_dir}/source" +export OPENLDAP_TEST_DN='dc=example,dc=org' + +prepare_ldif_files "${test_dir}/source" "${test_dir}/output" +test "$(cat "${test_dir}/output/10-entry.ldif")" = "dn: ${OPENLDAP_TEST_DN}" +test "$(cat "${test_dir}/output/20-plain.ldif")" = 'description: original' +test ! -e "${test_dir}/source/10-entry.ldif" +test "$(cat "${test_dir}/source/10-entry.ldif.template")" = "dn: \${OPENLDAP_TEST_DN}" +test -L "${test_dir}/source/10-entry.ldif.template" +test -z "$(find "${test_dir}/output" -name '.templates.*')" + +printf 'description: custom\n' > "${test_dir}/custom/20-plain.ldif" +prepare_ldif_files "${test_dir}/custom" "${test_dir}/output" +test "$(cat "${test_dir}/output/20-plain.ldif")" = 'description: custom' + +printf 'dn: %s\ndescription: custom template\n' "\${OPENLDAP_TEST_DN}" > "${test_dir}/custom/10-entry.ldif.template" +prepare_ldif_files "${test_dir}/custom" "${test_dir}/output" +test "$(cat "${test_dir}/output/10-entry.ldif")" = "$(printf 'dn: %s\ndescription: custom template' "${OPENLDAP_TEST_DN}")" +test ! -e "${test_dir}/custom/10-entry.ldif" + +# On failure, the bootstrap's global EXIT trap removes the working directory. +mkdir "${test_dir}/failure" +( + # Invoked by the child Bash process through the exported function. + # shellcheck disable=SC2329 + container() { + if [ "${1}" = envsubst ]; then + return 1 + fi + return 0 + } + export -f container + if bash -c 'set -euo pipefail; cleanup_dir=${3}; trap '\''rm -rf "${cleanup_dir:?}"'\'' EXIT; source "${1}"; prepare_ldif_files "${2}" "${3}"' -- \ + "${OPENLDAP_TEST_HELPER:-/container/services/openldap-bootstrap/assets/scripts/helpers/common.sh}" \ + "${test_dir}/source" "${test_dir}/failure"; then + exit 1 + fi +) +test ! -e "${test_dir}/failure" +test -z "$(find "${test_dir}/output" -name '.templates.*')" diff --git a/services/openldap-bootstrap/assets/scripts/README.md b/services/openldap-bootstrap/assets/scripts/README.md index d84dd6b..9d221dc 100644 --- a/services/openldap-bootstrap/assets/scripts/README.md +++ b/services/openldap-bootstrap/assets/scripts/README.md @@ -35,7 +35,7 @@ Utility functions shared across the bootstrap scripts. |----------|-------------| | `check_args` | Validates that a script was called with the expected operation (`env`/`ldif`) and argument count | | `prepare_env_file` | Appends exported variable assignments (safely shell-quoted) to an env file | -| `prepare_ldif_files` | Renders `.ldif.template` files via `container envsubst templates` and copies all resulting `.ldif` files (sorted) to the output directory | +| `prepare_ldif_files` | Copies sources to a temporary writable directory (dereferencing symlinks), renders `.ldif.template` files via `container envsubst templates`, then copies all resulting `.ldif` files (sorted) to the output directory. Sources can be mounted read-only. The function removes its working copy on success; the bootstrap's global EXIT trap cleans up on failure. | | `get_dn_attr` | Extracts the value of a named attribute from a DN string (e.g. `cn` from `cn=admin,dc=example,dc=com`) | ### `helpers/password.sh` diff --git a/services/openldap-bootstrap/assets/scripts/helpers/common.sh b/services/openldap-bootstrap/assets/scripts/helpers/common.sh index 63a3585..5f0bb8b 100755 --- a/services/openldap-bootstrap/assets/scripts/helpers/common.sh +++ b/services/openldap-bootstrap/assets/scripts/helpers/common.sh @@ -33,6 +33,7 @@ prepare_ldif_files() { local source_dir="$1" local output_dir="$2" + local work_dir if [ ! -d "${source_dir}" ]; then container log debug "Directory not found: ${source_dir}" @@ -41,13 +42,23 @@ prepare_ldif_files() { container log info "Load ${source_dir} LDIFs ..." - container log debug "Converting templates to LDIFs in ${source_dir} ..." - container envsubst templates "${source_dir}" 2>&1 | container log debug + work_dir=$(mktemp -d "${output_dir}/.templates.XXXXXX") + # output_dir is inside BOOTSTRAP_TMP_DIR, whose EXIT trap also cleans up + # this working copy if any command below fails. + + # Dereference projected-volume symlinks and make read-only sources writable + # only in the working copy. + cp -RL "${source_dir}/." "${work_dir}" + chmod -R u+rwX "${work_dir}" + + container log debug "Converting templates to LDIFs in ${work_dir} ..." + container envsubst templates "${work_dir}" 2>&1 | container log debug while IFS= read -r file; do cp -frav "${file}" "${output_dir}" 2>&1 | container log debug - done < <(find "${source_dir}" -type f -name "*.ldif" | sort -V) + done < <(find "${work_dir}" -type f -name "*.ldif" | sort -V) + rm -rf "${work_dir:?}" } get_dn_attr() {