326 lines
9.2 KiB
Bash
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})"
|