From 209036c036a5db5af33a78638df7e2c12b63a3ec Mon Sep 17 00:00:00 2001 From: lendry Date: Tue, 30 Jun 2026 17:01:19 +0300 Subject: [PATCH] fix and update --- infra/nginx/99-idp-gateway-watch.sh | 70 +++++++++------ install.sh | 132 +++++++++++++++++++++++++++- 2 files changed, 173 insertions(+), 29 deletions(-) diff --git a/infra/nginx/99-idp-gateway-watch.sh b/infra/nginx/99-idp-gateway-watch.sh index 2abae2a..e33ae00 100644 --- a/infra/nginx/99-idp-gateway-watch.sh +++ b/infra/nginx/99-idp-gateway-watch.sh @@ -1,52 +1,72 @@ #!/bin/sh -# После recreate api-gateway Docker выдаёт новый IP; без reload nginx шлёт на старый адрес → 502. +# После recreate edge-сервисов Docker выдаёт новый IP; без reload nginx шлёт на старый адрес → 502. set -eu -GW_HOST="${IDP_API_GATEWAY_HOST:-lendry-id-api-gateway}" -GW_PORT="${IDP_API_GATEWAY_PORT:-3000}" WATCH_INTERVAL="${IDP_NGINX_WATCH_INTERVAL:-5}" +WATCH_HOSTS="${IDP_NGINX_WATCH_HOSTS:-lendry-id-api-gateway:3000/health lendry-id-frontend:3000/}" + log() { echo "[idp-nginx-watch] $*" } -wait_for_gateway() { +probe_url() { + target="$1" + host="${target%%:*}" + rest="${target#*:}" + port="${rest%%/*}" + path="/${rest#*/}" + [ "$path" = "/${rest}" ] && path="/" + wget -qO- --timeout=3 "http://${host}:${port}${path}" 2>/dev/null | head -c 64 | grep -q . +} + +wait_for_upstreams() { attempt=0 while [ "$attempt" -lt 120 ]; do - if wget -qO- --timeout=3 "http://${GW_HOST}:${GW_PORT}/health" 2>/dev/null | grep -q '"status"'; then - log "api-gateway доступен (${GW_HOST}:${GW_PORT})" + ok=1 + for target in $WATCH_HOSTS; do + if ! probe_url "$target"; then + ok=0 + break + fi + done + if [ "$ok" -eq 1 ]; then + log "upstream-ы доступны" return 0 fi attempt=$((attempt + 1)) sleep 1 done - log "WARN: api-gateway не ответил за 120с — nginx стартует без ожидания" + log "WARN: не все upstream-ы ответили за 120с — nginx стартует без ожидания" return 0 } -watch_gateway() { - last_ip="" +watch_upstreams() { + last_ips="" while true; do sleep "$WATCH_INTERVAL" - current_ip="$(getent hosts "$GW_HOST" 2>/dev/null | awk '{print $1}' | head -n1 || true)" - healthy=0 - if wget -qO- --timeout=3 "http://${GW_HOST}:${GW_PORT}/health" 2>/dev/null | grep -q '"status"'; then - healthy=1 + reload=0 + current_ips="" + for target in $WATCH_HOSTS; do + host="${target%%:*}" + ip="$(getent hosts "$host" 2>/dev/null | awk '{print $1}' | head -n1 || true)" + current_ips="${current_ips}${host}=${ip};" + if ! probe_url "$target"; then + log "${host} недоступен — reload nginx" + reload=1 + break + fi + done + if [ "$reload" -eq 0 ] && [ -n "$last_ips" ] && [ "$current_ips" != "$last_ips" ]; then + log "IP upstream изменился — reload nginx" + reload=1 fi - if [ "$healthy" -eq 0 ]; then - log "api-gateway недоступен — reload nginx" - nginx -s reload 2>/dev/null || true - continue - fi - if [ -n "$current_ip" ] && [ -n "$last_ip" ] && [ "$current_ip" != "$last_ip" ]; then - log "IP ${GW_HOST} изменился (${last_ip} → ${current_ip}) — reload nginx" + if [ "$reload" -eq 1 ]; then nginx -s reload 2>/dev/null || true fi - if [ -n "$current_ip" ]; then - last_ip="$current_ip" - fi + last_ips="$current_ips" done } -wait_for_gateway -watch_gateway & +wait_for_upstreams +watch_upstreams & diff --git a/install.sh b/install.sh index 300c8bd..2066ba2 100644 --- a/install.sh +++ b/install.sh @@ -426,6 +426,20 @@ sync_nginx_after_api_gateway_restart() { fi } +# После recreate frontend/docs nginx должен перерезолвить lendry-id-frontend — иначе 502 на / (корень SSO). +sync_nginx_after_edge_restart() { + docker_nginx_container_running || return 0 + log "Синхронизация Docker Nginx с frontend после пересборки..." + wait_for_service "frontend" 120 || warn "frontend не стал healthy в срок" + sleep 2 + reload_docker_nginx + if ! verify_nginx_reaches_frontend 12 3; then + warn "Nginx → frontend: reload не помог — пересоздаём lendry-id-nginx..." + recreate_docker_nginx + verify_nginx_reaches_frontend 12 3 || warn "Nginx → frontend: проверка не прошла, см. docker compose logs nginx frontend" + fi +} + recreate_docker_nginx() { compose_uses_proxy_profile || return 0 ensure_nginx_gateway_watch_script @@ -483,11 +497,19 @@ post_deploy_proxy_verify() { } } verify_nginx_reaches_media_ws || true + verify_nginx_reaches_frontend 12 3 || { + [[ "$strict" -eq 1 ]] && fail "Docker Nginx не проксирует / на frontend — см. docker compose logs nginx frontend" + warn "Nginx → frontend: проверка не прошла" + } if [[ "$(env_get USE_NGINX_SSL false)" == "true" ]]; then check_sso_idp_api_proxy || { [[ "$strict" -eq 1 ]] && fail "SSO /idp-api/health → 502. Выполните: ./install.sh --fix-all" warn "SSO /idp-api/health не прошёл — выполните ./install.sh --fix-all" } + check_sso_frontend_root || { + [[ "$strict" -eq 1 ]] && fail "SSO / → 502. Nginx не проксирует главную на frontend. Выполните: ./install.sh --fix-all" + warn "SSO / (главная) не прошла — выполните ./install.sh --fix-all" + } fi } @@ -844,6 +866,10 @@ compose_build_and_up() { for svc in "${services[@]}"; do case "$svc" in api-gateway) sync_nginx_after_api_gateway_restart ;; + frontend|docs) + wait_for_service "$svc" 90 + sync_nginx_after_edge_restart + ;; media-ws) wait_for_service "media-ws" 60 reload_docker_nginx @@ -2716,6 +2742,96 @@ verify_nginx_idp_api_location() { return 1 } +verify_nginx_reaches_frontend() { + docker_nginx_container_running || return 0 + build_compose_stack_cmd + load_env + local max_attempts="${1:-1}" + local delay_sec="${2:-0}" + local attempt=0 + local body fe_ip frontend_domain code + + frontend_domain="$(env_get DOMAIN_FRONTEND "")" + + while [[ $attempt -lt $max_attempts ]]; do + fe_ip="$(docker_cmd inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' lendry-id-frontend 2>/dev/null || true)" + body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \ + wget -qO- --timeout=6 http://lendry-id-frontend:3000/ 2>/dev/null | head -c 256 || true)" + if [[ -n "$body" ]]; then + ok "Docker Nginx → lendry-id-frontend:3000/ OK (IP frontend: ${fe_ip:-?})" + if [[ -n "$frontend_domain" ]]; then + verify_nginx_frontend_root_location "$frontend_domain" || return 1 + fi + return 0 + fi + attempt=$((attempt + 1)) + if [[ $attempt -lt $max_attempts ]]; then + [[ "$delay_sec" -gt 0 ]] && sleep "$delay_sec" + fi + done + + warn "Docker Nginx не достучался до lendry-id-frontend:3000/ (IP: ${fe_ip:-?}, попыток: ${max_attempts})" + warn "Проверьте: docker compose --env-file .env --profile proxy logs --tail=40 frontend nginx" + return 1 +} + +# location / → frontend (путь браузера на https://sso.../) +verify_nginx_frontend_root_location() { + local frontend_domain="$1" + local body code ssl_type + ssl_type="$(env_get SSL_TYPE none)" + + body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \ + wget -qO- --timeout=8 --no-check-certificate \ + --header="Host: ${frontend_domain}" \ + "https://127.0.0.1/" 2>/dev/null | head -c 256 || true)" + if [[ -n "$body" ]]; then + ok "Docker Nginx location / → frontend OK (Host: ${frontend_domain})" + return 0 + fi + + if [[ "$(env_get USE_NGINX_SSL false)" == "true" || "$ssl_type" != "none" ]]; then + code="$(curl -sk --max-time 8 -o /dev/null -w '%{http_code}' "https://${frontend_domain}/" 2>/dev/null || echo 000)" + else + code="$(curl -fsS --max-time 8 -o /dev/null -w '%{http_code}' "http://${frontend_domain}/" 2>/dev/null || echo 000)" + fi + if [[ "$code" == "200" || "$code" == "304" ]]; then + ok "SSO / → HTTP ${code} (проверка с хоста)" + return 0 + fi + + warn "Nginx location / не проксирует на frontend (Host: ${frontend_domain}, HTTP ${code:-?})" + warn "Проверьте: curl -sk -o /dev/null -w '%{http_code}' https://${frontend_domain}/" + return 1 +} + +check_sso_frontend_root() { + load_env + local frontend_domain code ssl_type + frontend_domain="$(env_get DOMAIN_FRONTEND "")" + [[ -n "$frontend_domain" ]] || return 0 + ssl_type="$(env_get SSL_TYPE none)" + + if [[ "$(env_get USE_NGINX_SSL false)" == "true" || "$ssl_type" != "none" ]]; then + code="$(curl -sk --max-time 8 -o /dev/null -w '%{http_code}' "https://${frontend_domain}/" 2>/dev/null || echo 000)" + else + code="$(curl -fsS --max-time 8 -o /dev/null -w '%{http_code}' "http://${frontend_domain}/" 2>/dev/null || echo 000)" + fi + + if [[ "$code" == "200" || "$code" == "304" ]]; then + echo -e " ${GREEN}✔${NC} SSO / → ${code} (главная страница SPA)" + return 0 + elif [[ "$code" == "502" ]]; then + echo -e " ${RED}✘${NC} SSO / → 502 — Nginx не проксирует / на frontend (502 Bad Gateway в браузере)" + echo " Проверьте: docker compose --env-file .env --profile proxy exec nginx wget -qO- http://lendry-id-frontend:3000/" + echo " Решение: docker compose --env-file .env --profile proxy restart nginx frontend" + return 1 + else + echo -e " ${YELLOW}!${NC} SSO / → HTTP ${code} (ожидалось 200)" + return 1 + fi +} + verify_nginx_reaches_media_ws() { docker_nginx_container_running || return 0 build_compose_stack_cmd @@ -3205,10 +3321,14 @@ diagnose_common_issues() { echo -e " ${RED}✘${NC} Конфликт: системный Nginx на 80/443 и lendry-id-nginx одновременно — браузер может ходить не туда (502)" echo " Решение: sudo systemctl stop nginx && ./install.sh --fix-all" issues=$((issues + 1)) - elif [[ "$(env_get NGINX_MODE docker)" == "host" ]] \ - || port_in_use "${NGINX_HTTP_PORT:-80}" \ - || port_in_use "${NGINX_HTTPS_PORT:-443}"; then - echo -e " ${RED}✘${NC} Конфликт: контейнер lendry-id-nginx запущен при занятых портах 80/443 или NGINX_MODE=host" + elif [[ "$(env_get NGINX_MODE docker)" == "host" ]]; then + echo -e " ${RED}✘${NC} Конфликт: NGINX_MODE=host, но запущен lendry-id-nginx" + echo " Решение: ./install.sh --fix-all" + issues=$((issues + 1)) + elif [[ "$(env_get NGINX_MODE docker)" == "docker" ]] \ + && ! docker_nginx_container_running \ + && ( port_in_use "${NGINX_HTTP_PORT:-80}" || port_in_use "${NGINX_HTTPS_PORT:-443}" ); then + echo -e " ${RED}✘${NC} Порты 80/443 заняты, но lendry-id-nginx не запущен" echo " Решение: ./install.sh --fix-all" issues=$((issues + 1)) else @@ -3299,6 +3419,7 @@ diagnose_common_issues() { && [[ -n "$(env_get DOMAIN_API "")" && -n "$(env_get DOMAIN_FRONTEND "")" ]]; then check_https_api_with_origin || issues=$((issues + 1)) check_sso_idp_api_proxy || issues=$((issues + 1)) + check_sso_frontend_root || issues=$((issues + 1)) fi local api_domain frontend_domain public_api @@ -3677,12 +3798,15 @@ action_fix_all_errors() { apply_intranet_selfsigned_runtime recreate_docker_nginx sync_nginx_after_api_gateway_restart + sync_nginx_after_edge_restart verify_nginx_reaches_api_gateway 24 5 || compose_build_and_up api-gateway sync_nginx_after_api_gateway_restart verify_nginx_reaches_media_ws || compose_build_and_up media-ws reload_docker_nginx verify_nginx_reaches_api_gateway 24 5 || fail "Nginx не проксирует на api-gateway — см. docker compose logs nginx api-gateway" + verify_nginx_reaches_frontend 12 3 || fail "Nginx не проксирует / на frontend — см. docker compose logs nginx frontend" check_sso_idp_api_proxy || fail "SSO /idp-api/health недоступен — см. nginx/conf.d/${NGINX_PREFIX}-frontend.conf" + check_sso_frontend_root || fail "SSO / → 502 — главная страница недоступна (502 Bad Gateway в браузере)" if ! docker_nginx_container_running; then fail "Контейнер lendry-id-nginx не запущен. Проверьте: docker compose --profile proxy logs nginx" fi