From 1f8e00e4f9a5b6dad855248d3a5d89664b09337e Mon Sep 17 00:00:00 2001 From: Repinoid Date: Sun, 25 Oct 2026 17:43:42 +0300 Subject: [PATCH] feat: configure cloud API endpoints by stand --- README.md | 5 ++-- site/api/auth.py | 51 +++++++++++++---------------------------- site/api/http_client.py | 24 +++++++++++-------- site/app.py | 11 +++------ site/routes/api.py | 8 +++---- site/runner.py | 5 ---- tests/test_auth.py | 28 +++++++++++----------- 7 files changed, 53 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index f80c212..af83dcf 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,11 @@ Flask-приложение для автотестов сервисов Nubes. **jsonEnv:** ```json { - "NUBES_API_TOKEN": "...", - "NUBES_API_ENDPOINT": "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc" + "POLYGON_ENDPOINT": "https://polygon.pythonk8s.dev.nubes.ru/api/v1/svc" } ``` -Для PROD автоопределения дополнительно укажи `NUBES_API_PROD_ENDPOINT` с подтверждённым URL PROD API. Значение не задаётся по умолчанию; без него доступны только DEV/TEST из списка в `site/api/http_client.py`. +URL облачных API всех трёх стендов заданы в `site/api/http_client.py` (`API_ENDPOINTS`). Приложение проверяет пользовательский токен на DEV, TEST и PROD; если не находит подходящий стенд, использует TEST. Чтобы поменять URL, измени значение в `API_ENDPOINTS` и передеплой приложение. `NUBES_API_TOKEN` можно задать отдельно как необязательный fallback-токен; пользовательский токен вводится в UI. ## Структура diff --git a/site/api/auth.py b/site/api/auth.py index 564930f..2939edd 100644 --- a/site/api/auth.py +++ b/site/api/auth.py @@ -20,7 +20,7 @@ Cookie (устанавливаются main.py:action=set_mode): get_client() — HttpClient с учётом режима и стенда get_real_client() — алиас get_client() (историческая совместимость) get_client_id() — ClientID из JWT (base64url, без проверки подписи) - get_stand() — "polygon_test" / "dev" / "test" + get_stand() — "polygon_{stand}" / "dev" / "test" / "prod" get_token_info() — {email, company, client_id} из JWT для UI get_token_masked() — маскированный токен (abc...xyz) для placeholder """ @@ -29,7 +29,7 @@ import base64 import json from flask import request, current_app, g -from api.http_client import HttpClient, detect_endpoint, stand_name, STANDS +from api.http_client import DEFAULT_ENDPOINT, HttpClient, detect_endpoint, stand_name def _cached_detect(token): @@ -41,18 +41,7 @@ def _cached_detect(token): if cache is None: g._detected_endpoint = cache = {} if token not in cache: - endpoint = current_app.config["NUBES_API_ENDPOINT"].rstrip("/") - endpoints = [url.rstrip("/") for url in STANDS] - for configured_endpoint in ( - current_app.config.get("NUBES_API_PROD_ENDPOINT", ""), - endpoint, - ): - configured_endpoint = configured_endpoint.rstrip("/") - if configured_endpoint and configured_endpoint not in endpoints: - endpoints.append(configured_endpoint) - if endpoint in endpoints: - endpoint = detect_endpoint(token, endpoints=endpoints) or endpoint - cache[token] = endpoint + cache[token] = detect_endpoint(token) or DEFAULT_ENDPOINT return cache[token] @@ -89,25 +78,21 @@ def get_token(): return request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"] +def get_endpoint(): + """Вернуть URL текущего API: выбранный Polygon stand или облако по токену.""" + if get_mode() == "polygon": + polygon = current_app.config.get("POLYGON_ENDPOINT", "") + stand = get_polygon_stand() + return polygon.replace("/api/v1/svc", f"/{stand}/api/v1/svc") + return _cached_detect(get_token()) + + def get_client(): """HttpClient с учётом режима и стенда. Эмуляция: polygon_url + "/" + stand → https://polygon.../{stand}/api/v1/svc - Облако: реальный API с автоопределением стенда (dev/test по токену).""" - token = get_token() - mode = get_mode() - - if mode == "polygon": - polygon = current_app.config.get("POLYGON_ENDPOINT", "") - stand = get_polygon_stand() - # POLYGON_ENDPOINT = "https://polygon.../api/v1/svc" - # Стенд ВСТАВЛЯЕТСЯ перед /api/v1/svc: - # "https://polygon.../api/v1/svc" → "https://polygon.../test/api/v1/svc" - # Простая замена "/api/v1/svc" → "/{stand}/api/v1/svc" - return HttpClient(polygon.replace("/api/v1/svc", f"/{stand}/api/v1/svc"), token) - - # Облако: NUBES_API_ENDPOINT с автоопределением (кешированным) - return HttpClient(_cached_detect(token), token) + Облако: реальный API с автоопределением стенда (dev/test/prod по токену).""" + return HttpClient(get_endpoint(), get_token()) def get_client_id(): @@ -139,16 +124,12 @@ def get_stand(): """Определить стенд. Эмуляция: "polygon_{dev|test|prod}" — для load_service_ids и трекера. - Облако: автоопределение или stand_name (dev/test/mock).""" + Облако: автоопределение или stand_name (dev/test/prod/mock).""" mode = get_mode() if mode == "polygon": return f"polygon_{get_polygon_stand()}" - token = get_token() - endpoint = _cached_detect(token).rstrip("/") - prod_endpoint = current_app.config.get("NUBES_API_PROD_ENDPOINT", "").rstrip("/") - if prod_endpoint and endpoint == prod_endpoint: - return "prod" + endpoint = _cached_detect(get_token()) s = stand_name(endpoint) return s if s != "?" else "mock" diff --git a/site/api/http_client.py b/site/api/http_client.py index f26ad50..459631b 100644 --- a/site/api/http_client.py +++ b/site/api/http_client.py @@ -11,7 +11,7 @@ HttpClient: Функции автоопределения стенда: - detect_endpoint(token) — пробует настроенные API стенды по токену - - stand_name(endpoint) — "dev" / "test" по URL + - stand_name(endpoint) — возвращает имя стенда по URL - create_client(token) — HttpClient + endpoint одним вызовом Зачем автоопределение: пользователь вводит токен, мы не знаем его стенд. @@ -20,12 +20,16 @@ HttpClient: import requests -# Список стендов для автоопределения. -# Порядок ВАЖЕН: dev первый — он быстрее (меньше нагрузка), test — резервный. -STANDS = [ - "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc", - "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc", -] +# Источник истины для URL облачных API. Меняй значения здесь и передеплой, +# а не через NUBES_API_ENDPOINT в параметрах сервиса. +API_ENDPOINTS = { + "dev": "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc", + "test": "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc", + "prod": "https://lk-api-gateway.ngcloud.ru/api/v1/svc", +} +DEFAULT_STAND = "test" +DEFAULT_ENDPOINT = API_ENDPOINTS[DEFAULT_STAND] +STANDS = list(API_ENDPOINTS.values()) def detect_endpoint(token, endpoints=None): @@ -54,8 +58,8 @@ def detect_endpoint(token, endpoints=None): def stand_name(endpoint): """Определить имя стенда по URL — точное совпадение endpoint. - Возвращает 'dev'/'test' или '?' если не совпало.""" - for name, url in (("dev", STANDS[0]), ("test", STANDS[1])): + Возвращает имя стенда или '?' если URL неизвестен.""" + for name, url in API_ENDPOINTS.items(): if (endpoint or "").rstrip("/") == url.rstrip("/"): return name return "?" @@ -64,7 +68,7 @@ def stand_name(endpoint): def create_client(token, fallback_endpoint=None): """HttpClient с автоопределением стенда. - Сначала detect_endpoint(token) — dev→test. + Сначала detect_endpoint(token) — dev→test→prod. Если не сработало — fallback_endpoint (из конфига). Возвращает (HttpClient, endpoint_url) или None.""" ep = detect_endpoint(token) or fallback_endpoint diff --git a/site/app.py b/site/app.py index d34869b..e68399c 100644 --- a/site/app.py +++ b/site/app.py @@ -16,8 +16,8 @@ Blueprint'ы (каждый — отдельный файл в routes/): gunicorn app:app --workers 2 --bind 0.0.0.0:8000 Конфигурация: - NUBES_API_ENDPOINT — URL API (по умолчанию test) - NUBES_API_TOKEN — сервисный токен (из переменных окружения k8s) + NUBES_API_TOKEN — необязательный fallback-токен из переменных окружения + API_ENDPOINTS — URL DEV/TEST/PROD в api/http_client.py VERSION — версия приложения (меняется при КАЖДОМ изменении кода) """ @@ -32,19 +32,14 @@ from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp # Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода. # Нужна для фильтрации истории (пользователь видит только записи своей версии). -VERSION = "1.2.49" +VERSION = "1.2.50" # Flask-приложение с Jinja2-шаблонами из папки templates/ app = Flask(__name__, template_folder="templates", static_folder="static") # Конфигурация из переменных окружения (задаются в Nubes UI при создании сервиса) -app.config["NUBES_API_ENDPOINT"] = os.getenv( - "NUBES_API_ENDPOINT", - "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc" -) app.config["NUBES_API_TOKEN"] = os.getenv("NUBES_API_TOKEN", "") app.config["POLYGON_ENDPOINT"] = os.getenv("POLYGON_ENDPOINT", "") # если задан → все запросы в polygon -app.config["NUBES_API_PROD_ENDPOINT"] = os.getenv("NUBES_API_PROD_ENDPOINT", "") app.config["VERSION"] = VERSION # Регистрируем blueprint'ы — каждый отвечает за свою группу маршрутов diff --git a/site/routes/api.py b/site/routes/api.py index b04b28a..156bf09 100644 --- a/site/routes/api.py +++ b/site/routes/api.py @@ -1,8 +1,8 @@ import threading -from flask import Blueprint, current_app, jsonify, request +from flask import Blueprint, jsonify, request -from api.auth import get_client +from api.auth import get_client, get_endpoint, get_token from runner import run_tests, get_status, load_config, save_config bp = Blueprint("api", __name__) @@ -10,11 +10,11 @@ bp = Blueprint("api", __name__) @bp.route("/api/run", methods=["POST"]) def api_run(): - token = current_app.config["NUBES_API_TOKEN"] + token = get_token() client = get_client() - endpoint = current_app.config["NUBES_API_ENDPOINT"] if not client: return jsonify({"error": "Не удалось определить стенд"}), 500 + endpoint = get_endpoint() t = threading.Thread( target=run_tests, args=(endpoint, token), diff --git a/site/runner.py b/site/runner.py index eb709c1..3c7483c 100644 --- a/site/runner.py +++ b/site/runner.py @@ -104,11 +104,6 @@ def run_tests(endpoint, token): endpoint: str — URL API token: str — JWT-токен""" global _current_run - # POLYGON_ENDPOINT проверяется в get_client(), здесь — прямой вызов для CLI - import os - polygon = os.getenv("POLYGON_ENDPOINT", "") - if polygon: - endpoint = polygon client = HttpClient(endpoint, token) config = load_config() diff --git a/tests/test_auth.py b/tests/test_auth.py index 83029cf..a4e9c04 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -2,6 +2,12 @@ Тесты: auth.py — режимы, стенды, URL, cookie. """ import api.auth as auth +from api.http_client import API_ENDPOINTS, STANDS + + +def test_api_endpoint_map_has_all_three_stands(): + assert list(API_ENDPOINTS) == ["dev", "test", "prod"] + assert len(STANDS) == 3 def test_polygon_url_construction(monkeypatch): @@ -125,18 +131,15 @@ def test_whitelist_stand_invalid(monkeypatch): assert auth.get_polygon_stand() == "test" # fallback -def test_cloud_detection_includes_configured_prod_endpoint(monkeypatch): +def test_cloud_detection_includes_prod_endpoint_from_source_map(monkeypatch): from flask import Flask - prod_endpoint = "https://prod.example/api/v1/svc" + prod_endpoint = API_ENDPOINTS["prod"] app = Flask(__name__) - app.config["NUBES_API_ENDPOINT"] = auth.STANDS[1] - app.config["NUBES_API_PROD_ENDPOINT"] = prod_endpoint captured = {} - def fake_detect(token, endpoints): + def fake_detect(token): captured["token"] = token - captured["endpoints"] = endpoints return prod_endpoint monkeypatch.setattr(auth, "detect_endpoint", fake_detect) @@ -145,14 +148,13 @@ def test_cloud_detection_includes_configured_prod_endpoint(monkeypatch): assert auth._cached_detect("prod-token") == prod_endpoint assert captured["token"] == "prod-token" - assert captured["endpoints"] == [*auth.STANDS, prod_endpoint] -def test_detect_endpoint_tries_configured_prod_endpoint(monkeypatch): +def test_detect_endpoint_tries_prod_endpoint(monkeypatch): from api import http_client - prod_endpoint = "https://prod.example/api/v1/svc" - endpoints = [*auth.STANDS, prod_endpoint] + prod_endpoint = API_ENDPOINTS["prod"] + endpoints = STANDS attempted = [] class FakeHttpClient: @@ -171,13 +173,11 @@ def test_detect_endpoint_tries_configured_prod_endpoint(monkeypatch): assert attempted == endpoints -def test_get_stand_labels_configured_prod_endpoint(monkeypatch): +def test_get_stand_labels_prod_endpoint(monkeypatch): from flask import Flask - prod_endpoint = "https://prod.example/api/v1/svc" + prod_endpoint = API_ENDPOINTS["prod"] app = Flask(__name__) - app.config["NUBES_API_ENDPOINT"] = auth.STANDS[1] - app.config["NUBES_API_PROD_ENDPOINT"] = prod_endpoint monkeypatch.setattr(auth, "get_mode", lambda: "cloud") monkeypatch.setattr(auth, "get_token", lambda: "prod-token") monkeypatch.setattr(auth, "_cached_detect", lambda token: prod_endpoint)