diff --git a/nginx/.reload-in-container.sh b/nginx/.reload-in-container.sh new file mode 100644 index 0000000..abf1b1c --- /dev/null +++ b/nginx/.reload-in-container.sh @@ -0,0 +1,130 @@ +#!/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" + +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 + if [ "$finish_status" -eq 0 ] && [ "$2" = committed ]; then + echo "Nginx configuration reloaded successfully." + fi + exit "$finish_status" +} + +main() { + 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 "$@" diff --git a/nginx/reload.sh b/nginx/reload.sh index c1c461f..4ab3d7e 100755 --- a/nginx/reload.sh +++ b/nginx/reload.sh @@ -1,102 +1,7 @@ #!/usr/bin/env bash -set -Eeuo pipefail +set -euo pipefail cd -- "$(dirname -- "${BASH_SOURCE[0]}")" -readonly SERVICE="nginx" - -if ! docker compose exec -T "$SERVICE" true >/dev/null 2>&1; then - echo "Error: nginx container is not running." >&2 - exit 1 -fi - -docker compose exec -T "$SERVICE" sh <<'EOF' -set -eu - -TEMPLATE_DIR="/etc/nginx/templates" -OUTPUT_DIR="/etc/nginx" -ENVSUBST_SCRIPT="/docker-entrypoint.d/20-envsubst-on-templates.sh" -MANAGED_DIRS="conf.d snippets" - -if [ ! -d "$TEMPLATE_DIR" ]; then - echo "Error: template directory does not exist: $TEMPLATE_DIR" >&2 - exit 1 -fi - -if [ ! -x "$ENVSUBST_SCRIPT" ]; then - echo "Error: envsubst script is not executable: $ENVSUBST_SCRIPT" >&2 - exit 1 -fi - -# Only allow templates in directories managed by this script. -find "$TEMPLATE_DIR" -type f -name '*.template' -print | while IFS= read -r template; do - relative="${template#"$TEMPLATE_DIR"/}" - - case "$relative" in - conf.d/* | snippets/*) - ;; - *) - echo "Error: unmanaged template path: $relative" >&2 - exit 1 - ;; - esac -done - -backup_dir="$(mktemp -d /tmp/nginx-reload.XXXXXX)" -committed=0 - -rollback() { - status=$? - - if [ "$committed" -eq 0 ]; then - echo "Reload failed. Restoring previous generated configuration." >&2 - - for dir in $MANAGED_DIRS; do - rm -rf "$OUTPUT_DIR/$dir" - - if [ -d "$backup_dir/$dir" ]; then - cp -a "$backup_dir/$dir" "$OUTPUT_DIR/$dir" - fi - done - fi - - rm -rf "$backup_dir" - exit "$status" -} - -trap rollback EXIT HUP INT TERM - -for dir in $MANAGED_DIRS; do - if [ -d "$OUTPUT_DIR/$dir" ]; then - cp -a "$OUTPUT_DIR/$dir" "$backup_dir/$dir" - fi -done - -for dir in $MANAGED_DIRS; do - if [ -d "$OUTPUT_DIR/$dir" ]; then - find "$OUTPUT_DIR/$dir" -type f -print | while IFS= read -r output; do - relative="${output#"$OUTPUT_DIR"/}" - - if [ ! -f "$TEMPLATE_DIR/$relative.template" ]; then - echo "Removing stale generated file: $relative" - fi - done - fi - - rm -rf "$OUTPUT_DIR/$dir" - mkdir -p "$OUTPUT_DIR/$dir" -done - -"$ENVSUBST_SCRIPT" - -nginx -t - -committed=1 -trap - EXIT HUP INT TERM -rm -rf "$backup_dir" -EOF - -docker compose exec -T "$SERVICE" nginx -s reload - -echo "Nginx configuration reloaded successfully." +exec docker compose exec -T nginx sh -s <./.reload-in-container.sh