gongxiangp2/scripts/local-proxy-reconcile.sh

326 lines
9.2 KiB
Bash

#!/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})"