#!/usr/bin/env bash set -euo pipefail CONTAINER_NAME="${CONTAINER_NAME:?CONTAINER_NAME is required}" PORT="${PORT:?PORT is required}" PROXY_ROOT="${PROXY_ROOT:?PROXY_ROOT is required}" PROXY_SERVICE="${PROXY_SERVICE:?PROXY_SERVICE is required}" CURRENT_FILE="${PROXY_ROOT}/current-container" PROXY_CONF="${PROXY_ROOT}/nginx.conf" LOCK_FILE="${PROXY_RECONCILE_LOCK_FILE:-/run/${CONTAINER_NAME}-local-proxy-reconcile.lock}" NGINX_BIN="${NGINX_BIN:-/usr/sbin/nginx}" BLUE_CONTAINER="${CONTAINER_NAME}-blue" GREEN_CONTAINER="${CONTAINER_NAME}-green" log() { printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" } container_running() { [ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null || true)" = "true" ] } container_ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$1" 2>/dev/null || true } container_healthy() { local name="$1" local ip="" container_running "$name" || return 1 ip="$(container_ip "$name")" [ -n "$ip" ] || return 1 curl -fsS --max-time 5 "http://${ip}:3000/api/health" >/dev/null 2>&1 } container_for_ip() { local expected_ip="$1" local name="" local ip="" [ -n "$expected_ip" ] || return 1 for name in "$BLUE_CONTAINER" "$GREEN_CONTAINER"; do container_running "$name" || continue ip="$(container_ip "$name")" if [ "$ip" = "$expected_ip" ]; then printf '%s\n' "$name" return 0 fi done return 1 } read_current_container() { local name="" if [ -s "$CURRENT_FILE" ]; then name="$(tr -d '\r\n' < "$CURRENT_FILE")" fi case "$name" in "$BLUE_CONTAINER"|"$GREEN_CONTAINER") printf '%s\n' "$name" ;; *) printf '\n' ;; esac } write_current_container() { local name="$1" local temp_file="" temp_file="$(mktemp "${CURRENT_FILE}.tmp.XXXXXX")" printf '%s\n' "$name" > "$temp_file" chmod 0644 "$temp_file" mv "$temp_file" "$CURRENT_FILE" } configured_ip() { sed -nE 's/^[[:space:]]*server ([0-9.]+):3000 .*/\1/p' "$PROXY_CONF" | head -n 1 } configured_header_container() { local target="" target="$( sed -nE 's/^[[:space:]]*add_header X-Material-Proxy-Target ([^;[:space:]]+) always;.*/\1/p' "$PROXY_CONF" | tail -n 1 )" case "$target" in "$BLUE_CONTAINER"|"$GREEN_CONTAINER") printf '%s\n' "$target" ;; *) printf '\n' ;; esac } configured_container() { local header_container="" local ip_container="" header_container="$(configured_header_container)" ip_container="$(container_for_ip "$(configured_ip)" 2>/dev/null || true)" if [ -n "$header_container" ] && [ -n "$ip_container" ] && [ "$header_container" != "$ip_container" ]; then printf '\n' elif [ -n "$header_container" ]; then printf '%s\n' "$header_container" else printf '%s\n' "$ip_container" fi } runtime_proxy_container() { local target="" target="$( curl -sS --max-time 5 -D - -o /dev/null "http://127.0.0.1:${PORT}/api/health" 2>/dev/null | tr -d '\r' | sed -nE 's/^X-Material-Proxy-Target:[[:space:]]*(.*)$/\1/Ip' | tail -n 1 || true )" case "$target" in "$BLUE_CONTAINER"|"$GREEN_CONTAINER") printf '%s\n' "$target" ;; *) printf '\n' ;; esac } entry_healthy() { curl -fsS --max-time 5 "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1 } verify_runtime_target() { local expected_container="$1" local observed_container="" for _ in $(seq 1 5); do observed_container="$(runtime_proxy_container)" if [ "$observed_container" = "$expected_container" ] && entry_healthy; then return 0 fi sleep 1 done return 1 } reload_proxy() { if systemctl is-active --quiet "$PROXY_SERVICE"; then systemctl reload "$PROXY_SERVICE" else systemctl start "$PROXY_SERVICE" fi } backup="" write_proxy_config_for_target() { local target_container="$1" local target_ip="$2" local temp_conf="" backup="" temp_conf="$(mktemp "${PROXY_CONF}.tmp.XXXXXX")" trap 'if [ -n "${temp_conf:-}" ]; then rm -f "$temp_conf"; fi' EXIT if ! sed -E \ -e "0,/server [0-9.]+:3000 /s//server ${target_ip}:3000 /" \ -e '/^[[:space:]]*proxy_hide_header X-Material-Proxy-Target;[[:space:]]*$/d' \ -e '/^[[:space:]]*add_header X-Material-Proxy-Target [^;]+ always;[[:space:]]*$/d' \ "$PROXY_CONF" | awk -v target="$target_container" ' { print } /^[[:space:]]*location \/ \{/ { print " proxy_hide_header X-Material-Proxy-Target;" print " add_header X-Material-Proxy-Target " target " always;" } ' > "$temp_conf"; then return 1 fi if ! grep -q "server ${target_ip}:3000 " "$temp_conf" || ! grep -q "add_header X-Material-Proxy-Target ${target_container} always;" "$temp_conf"; then log "ERROR" "failed to prepare nginx configuration for ${target_container}" return 1 fi if ! "$NGINX_BIN" -t -p "${PROXY_ROOT}/" -c "$temp_conf"; then return 1 fi backup="${PROXY_CONF}.bak.reconcile.$(date +%Y%m%d-%H%M%S)" cp -a "$PROXY_CONF" "$backup" || return 1 mv "$temp_conf" "$PROXY_CONF" || return 1 trap - EXIT temp_conf="" } restore_previous_proxy() { local restore_temp="" if [ -n "$backup" ] && [ -f "$backup" ]; then restore_temp="$(mktemp "${PROXY_CONF}.restore.XXXXXX")" || return 1 if ! cp -a "$backup" "$restore_temp"; then rm -f "$restore_temp" return 1 fi if ! "$NGINX_BIN" -t -p "${PROXY_ROOT}/" -c "$restore_temp" >/dev/null 2>&1; then rm -f "$restore_temp" return 1 fi if ! mv "$restore_temp" "$PROXY_CONF"; then rm -f "$restore_temp" return 1 fi reload_proxy >/dev/null 2>&1 || return 1 fi } command -v flock >/dev/null 2>&1 || { log "ERROR" "flock is required" exit 1 } command -v awk >/dev/null 2>&1 || { log "ERROR" "awk is required" exit 1 } exec 9>"$LOCK_FILE" flock -n 9 || exit 0 if [ ! -s "$PROXY_CONF" ]; then log "WARN" "proxy configuration is missing: ${PROXY_CONF}" exit 1 fi current_container="$(read_current_container)" configured_container_value="$(configured_container)" runtime_container="$(runtime_proxy_container)" entry_ok=0 if entry_healthy; then entry_ok=1 fi target_container="" if [ "$entry_ok" = "1" ]; then if [ -n "$runtime_container" ] && container_healthy "$runtime_container"; then target_container="$runtime_container" elif [ -n "$current_container" ] && [ -n "$configured_container_value" ] && [ "$current_container" != "$configured_container_value" ] && container_healthy "$current_container" && container_healthy "$configured_container_value"; then log "ERROR" "healthy entry has no runtime marker and state/config point to different healthy containers" exit 1 elif [ -n "$current_container" ] && container_healthy "$current_container"; then # Without a runtime marker, preserve the last verified target instead of # trusting a disk config that may have been written before a failed reload. target_container="$current_container" elif [ -n "$configured_container_value" ] && container_healthy "$configured_container_value"; then target_container="$configured_container_value" fi else if [ -n "$runtime_container" ] && container_healthy "$runtime_container"; then target_container="$runtime_container" elif [ -n "$configured_container_value" ] && container_healthy "$configured_container_value"; then target_container="$configured_container_value" elif [ -n "$current_container" ] && container_healthy "$current_container"; then target_container="$current_container" fi fi if [ -z "$target_container" ]; then healthy_containers=() for name in "$BLUE_CONTAINER" "$GREEN_CONTAINER"; do if container_healthy "$name"; then healthy_containers+=("$name") fi done if [ "${#healthy_containers[@]}" -eq 1 ]; then target_container="${healthy_containers[0]}" elif [ "${#healthy_containers[@]}" -gt 1 ]; then log "ERROR" "multiple healthy containers found without an authoritative runtime target" exit 1 fi fi if [ -z "$target_container" ]; then log "WARN" "no healthy material container is available" exit 1 fi target_ip="$(container_ip "$target_container")" if [ -z "$target_ip" ]; then log "WARN" "target container has no network address: ${target_container}" exit 1 fi if [ "$entry_ok" = "1" ] && [ "$runtime_container" = "$target_container" ] && [ "$configured_container_value" = "$target_container" ]; then if [ "$current_container" != "$target_container" ]; then write_current_container "$target_container" log "OK" "state aligned to verified runtime target: ${target_container}" fi exit 0 fi if ! write_proxy_config_for_target "$target_container" "$target_ip"; then log "ERROR" "failed to prepare proxy configuration for ${target_container}" exit 1 fi reload_reported_failure=0 if ! reload_proxy; then reload_reported_failure=1 fi if ! verify_runtime_target "$target_container"; then restore_previous_proxy || true log "ERROR" "failed to verify proxy target ${target_container}; previous configuration was retained when possible" exit 1 fi write_current_container "$target_container" if [ "$reload_reported_failure" = "1" ]; then log "WARN" "reload command reported failure, but the runtime target was independently verified" fi log "OK" "proxy reconciled to verified runtime target: ${target_container} (${target_ip})"