#!/bin/sh set -eu # Share template paths with the Nginx image's envsubst entrypoint. readonly TEMPLATE_DIR="${NGINX_ENVSUBST_TEMPLATE_DIR:-/etc/nginx/templates}" readonly TEMPLATE_SUFFIX="${NGINX_ENVSUBST_TEMPLATE_SUFFIX:-.template}" readonly OUTPUT_DIR="${NGINX_ENVSUBST_OUTPUT_DIR:-/etc/nginx}" readonly ENVSUBST_SCRIPT="${NGINX_RELOAD_ENVSUBST_SCRIPT:-/docker-entrypoint.d/20-envsubst-on-templates.sh}" readonly MANAGED_DIRS="conf.d snippets" readonly LOCK_FILE="${NGINX_RELOAD_LOCK_FILE:-/tmp/nginx-reload.lock}" acquire_lock() { if ! command -v flock >/dev/null 2>&1; then echo "Error: flock is required to serialize configuration reloads." >&2 return 1 fi # Keep FD 9 open through rollback and cleanup; never unlink the lock file. exec 9>"$LOCK_FILE" if ! flock -n 9; then echo "Error: cannot acquire reload lock; another reload may be running: $LOCK_FILE" >&2 return 1 fi } is_managed_template() { for managed_dir in $MANAGED_DIRS; do case "$1" in "$managed_dir"/*) return 0 ;; esac done return 1 } validate_templates() { if [ ! -d "$TEMPLATE_DIR" ]; then echo "Error: template directory does not exist: $TEMPLATE_DIR" >&2 return 1 fi if [ ! -x "$ENVSUBST_SCRIPT" ]; then echo "Error: envsubst script is not executable: $ENVSUBST_SCRIPT" >&2 return 1 fi # Collect first so a failed find cannot be masked by a pipeline. templates="$(find "$TEMPLATE_DIR" -follow -type f -name "*$TEMPLATE_SUFFIX" -print)" printf '%s\n' "$templates" | while IFS= read -r template; do [ -n "$template" ] || continue relative="${template#"$TEMPLATE_DIR"/}" if ! is_managed_template "$relative"; then echo "Error: unmanaged template path: $relative" >&2 exit 1 fi done } backup_config() { for dir in $MANAGED_DIRS; do if [ -d "$OUTPUT_DIR/$dir" ]; then cp -a "$OUTPUT_DIR/$dir" "$1/$dir" fi done } generate_config() { for dir in $MANAGED_DIRS; do if [ -d "$OUTPUT_DIR/$dir" ]; then outputs="$(find "$OUTPUT_DIR/$dir" -type f -print)" printf '%s\n' "$outputs" | while IFS= read -r output; do [ -n "$output" ] || continue relative="${output#"$OUTPUT_DIR"/}" if [ ! -f "$TEMPLATE_DIR/$relative$TEMPLATE_SUFFIX" ]; then echo "Removing stale generated file: $relative" fi done fi rm -rf "$OUTPUT_DIR/$dir" mkdir -p "$OUTPUT_DIR/$dir" done NGINX_ENVSUBST_TEMPLATE_DIR="$TEMPLATE_DIR" \ NGINX_ENVSUBST_TEMPLATE_SUFFIX="$TEMPLATE_SUFFIX" \ NGINX_ENVSUBST_OUTPUT_DIR="$OUTPUT_DIR" \ "$ENVSUBST_SCRIPT" } restore_config() { restore_failed=0 for dir in $MANAGED_DIRS; do if ! rm -rf "$OUTPUT_DIR/$dir"; then restore_failed=1 continue fi if [ -d "$1/$dir" ]; then cp -a "$1/$dir" "$OUTPUT_DIR/$dir" || restore_failed=1 fi done [ "$restore_failed" -eq 0 ] } finish() { finish_status="$1" trap - EXIT trap '' HUP INT TERM if [ "$2" = modifying ]; then echo "Reload failed. Restoring previous generated configuration." >&2 [ "$finish_status" -ne 0 ] || finish_status=1 if ! restore_config "$3"; then echo "Error: configuration rollback incomplete. Backup preserved at: $3" >&2 exit "$finish_status" fi fi if ! rm -rf "$3"; then echo "Error: cannot remove configuration backup: $3" >&2 [ "$finish_status" -ne 0 ] || finish_status=1 fi exit "$finish_status" } main() { acquire_lock validate_templates backup_dir="$(mktemp -d /tmp/nginx-reload.XXXXXX)" transaction_state=backing_up trap 'finish "$?" "$transaction_state" "$backup_dir"' EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM backup_config "$backup_dir" # Only a complete backup permits mutation and subsequent restoration. transaction_state=modifying generate_config nginx -t nginx -s reload transaction_state=committed } main "$@"