Реализация на отдельных двух машинах с протестированным взаимодействием по проверке сообщений

This commit is contained in:
mi
2026-08-19 18:24:00 +03:00
parent bbef7a30c9
commit c7a80e7256
103 changed files with 3457 additions and 3725 deletions
+36 -14
View File
@@ -2,6 +2,7 @@
"""Validate public deployment config separately from runtime secrets."""
from __future__ import annotations
import ipaddress
import os
import re
import stat
@@ -16,15 +17,16 @@ REQUIRED_CONFIG = {
"PUBLIC_API_URL", "PUBLIC_AUTH_URL", "KEYCLOAK_PUBLIC_URL",
"KEYCLOAK_INTERNAL_URL", "KEYCLOAK_REALM", "KEYCLOAK_SMS_SERVICE_URL",
"IDGTL_SMS_BASE_URL", "IDGTL_SMS_CALLBACK_PUBLIC_URL",
"MESSAGE_SAFETY_URL", "MESSAGE_SAFETY_EXTRA_HOST",
"MESSAGE_SAFETY_CA_HOST_PATH", "MESSAGE_SAFETY_API_PREFIX",
}
REQUIRED_RUNTIME = {
"DATABASE_URL", "BITRIX_DATABASE_URL", "BITRIX_SYNC_DATABASE_URL",
"DATABASE_URL", "BITRIX_DATABASE_URL",
"SMS_DATABASE_URL", "KEYCLOAK_DB_PASSWORD", "REDIS_URL",
"REDIS_REALTIME_URL", "MESSAGE_SAFETY_REDIS_URL", "REDIS_API_PASSWORD",
"REDIS_SAFETY_PASSWORD", "REDIS_HEALTH_PASSWORD",
"REDIS_REALTIME_URL", "REDIS_API_PASSWORD", "REDIS_HEALTH_PASSWORD",
"MESSAGE_SAFETY_SERVICE_TOKEN", "BITRIX_LOCAL_APP_INTERNAL_TOKEN",
"BITRIX_INTERNAL_API_TOKEN", "BITRIX_API_FORWARD_TOKEN",
"BITRIX_API_INBOX_TOKEN", "BITRIX_SYNC_SERVICE_TOKEN",
"BITRIX_API_INBOX_TOKEN",
"KEYCLOAK_SETTINGS_BRIDGE_TOKEN", "KEYCLOAK_OTP_HMAC_KEY",
"KEYCLOAK_SMS_SERVICE_TOKEN", "SMS_SERVICE_TOKEN",
"IDGTL_SMS_API_KEY", "IDGTL_SMS_CALLBACK_USERNAME",
@@ -34,18 +36,15 @@ REQUIRED_RUNTIME = {
}
OPTIONAL_SECRET_KEYS = {
"BITRIX_CLIENT_SECRET", "BITRIX_APPLICATION_TOKEN",
"BITRIX_SYNC_CRM_WEBHOOK_URL", "BITRIX_SYNC_WEBHOOK_TOKEN",
"KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY",
"NOTIFICATIONS_TOKEN_PRODUCER_TEST",
"OTEL_REMOTE_AUTH_HEADER", "KEYCLOAK_OTP_MOCK_CODE",
}
FORBIDDEN_CONFIG_KEYS = REQUIRED_RUNTIME | OPTIONAL_SECRET_KEYS | {
"BITRIX_SYNC_APP_DATABASE_URL", "PG_BACKUP_DSN",
"PG_BACKUP_DSN",
}
DSN_KEYS = {
"DATABASE_URL", "BITRIX_DATABASE_URL", "BITRIX_SYNC_APP_DATABASE_URL",
"BITRIX_SYNC_DATABASE_URL", "MESSAGE_SAFETY_DATABASE_URL",
"SMS_DATABASE_URL", "PG_BACKUP_DSN",
"DATABASE_URL", "BITRIX_DATABASE_URL", "SMS_DATABASE_URL", "PG_BACKUP_DSN",
}
PLACEHOLDER = re.compile(r"(change-me|example\.(com|ru|invalid)|<[^>]+>)", re.I)
@@ -148,7 +147,6 @@ def validate_runtime(config: dict[str, str], runtime: dict[str, str], errors: li
redis_contract = (
("REDIS_URL", "api_backend", "REDIS_API_PASSWORD", "/0"),
("REDIS_REALTIME_URL", "api_backend", "REDIS_API_PASSWORD", "/1"),
("MESSAGE_SAFETY_REDIS_URL", "message_safety", "REDIS_SAFETY_PASSWORD", "/2"),
)
for key, username, password_key, database in redis_contract:
parsed = urlparse(runtime.get(key, ""))
@@ -180,23 +178,47 @@ def validate_shared(env: dict[str, str], errors: list[str]) -> None:
):
if env.get(key) and urlparse(env[key]).scheme != "https":
errors.append(f"{key}: публичный URL должен использовать https")
public_origin = f"https://{env.get('PUBLIC_HOST', '')}"
expected_public_urls = {
"PUBLIC_WEB_URL": public_origin,
"PUBLIC_API_URL": f"{public_origin}/api",
"PUBLIC_AUTH_URL": f"{public_origin}/auth",
"KEYCLOAK_PUBLIC_URL": f"{public_origin}/auth",
}
for key, expected in expected_public_urls.items():
if env.get(key, "").rstrip("/") != expected:
errors.append(f"{key}: должен быть производным от PUBLIC_HOST ({expected})")
for key in (
"KEYCLOAK_INTERNAL_URL", "KEYCLOAK_SMS_SERVICE_URL",
"MESSAGE_SAFETY_URL", "BITRIX_LOCAL_APP_BASE_URL",
"BITRIX_LOCAL_APP_BASE_URL",
):
parsed = urlparse(env.get(key, ""))
if parsed.scheme != "http" or "." in (parsed.hostname or ""):
errors.append(f"{key}: ожидается http URL с Docker DNS service name")
if not env.get("KEYCLOAK_INTERNAL_URL", "").rstrip("/").endswith("/auth"):
errors.append("KEYCLOAK_INTERNAL_URL: внутренний URL должен заканчиваться на /auth")
if env.get("MESSAGE_SAFETY_URL", "").rstrip("/") != "https://processing.internal:8443":
errors.append(
"MESSAGE_SAFETY_URL: ожидается remote TLS endpoint "
"https://processing.internal:8443"
)
try:
extra_host, extra_ip = env.get("MESSAGE_SAFETY_EXTRA_HOST", "").rsplit("=", 1)
safety_host = urlparse(env.get("MESSAGE_SAFETY_URL", "")).hostname
address = ipaddress.ip_address(extra_ip)
if extra_host != safety_host or not address.is_private:
raise ValueError
except ValueError:
errors.append(
"MESSAGE_SAFETY_EXTRA_HOST: ожидается <Safety hostname>=<private VM2 IP>"
)
if env.get("MESSAGE_SAFETY_API_PREFIX") != "/internal/safety/v2":
errors.append("MESSAGE_SAFETY_API_PREFIX: ожидается /internal/safety/v2")
try:
poll = int(env.get("MESSAGE_SAFETY_TASK_POLL_MAX_SEC", "0"))
nginx = int(env.get("NGINX_MESSAGE_READ_TIMEOUT_SEC", "0"))
ttl = int(env.get("MESSAGE_SAFETY_TASK_TTL_SEC", "0"))
if nginx < poll + 30:
errors.append("NGINX_MESSAGE_READ_TIMEOUT_SEC должен быть >= poll max + 30")
if ttl <= poll + 60:
errors.append("MESSAGE_SAFETY_TASK_TTL_SEC должен превышать poll max + recovery margin")
except ValueError:
errors.append("Safety/NGINX timeout values должны быть целыми числами")
if production and env.get("KEYCLOAK_OTP_MOCK_ENABLED", "").lower() == "true":