feat: configure cloud API endpoints by stand
This commit is contained in:
@@ -28,12 +28,11 @@ Flask-приложение для автотестов сервисов Nubes.
|
|||||||
**jsonEnv:**
|
**jsonEnv:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"NUBES_API_TOKEN": "...",
|
"POLYGON_ENDPOINT": "https://polygon.pythonk8s.dev.nubes.ru/api/v1/svc"
|
||||||
"NUBES_API_ENDPOINT": "https://lk-api-gateway-test.ngcloud.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.
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
|
|||||||
+16
-35
@@ -20,7 +20,7 @@ Cookie (устанавливаются main.py:action=set_mode):
|
|||||||
get_client() — HttpClient с учётом режима и стенда
|
get_client() — HttpClient с учётом режима и стенда
|
||||||
get_real_client() — алиас get_client() (историческая совместимость)
|
get_real_client() — алиас get_client() (историческая совместимость)
|
||||||
get_client_id() — ClientID из JWT (base64url, без проверки подписи)
|
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_info() — {email, company, client_id} из JWT для UI
|
||||||
get_token_masked() — маскированный токен (abc...xyz) для placeholder
|
get_token_masked() — маскированный токен (abc...xyz) для placeholder
|
||||||
"""
|
"""
|
||||||
@@ -29,7 +29,7 @@ import base64
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import request, current_app, g
|
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):
|
def _cached_detect(token):
|
||||||
@@ -41,18 +41,7 @@ def _cached_detect(token):
|
|||||||
if cache is None:
|
if cache is None:
|
||||||
g._detected_endpoint = cache = {}
|
g._detected_endpoint = cache = {}
|
||||||
if token not in cache:
|
if token not in cache:
|
||||||
endpoint = current_app.config["NUBES_API_ENDPOINT"].rstrip("/")
|
cache[token] = detect_endpoint(token) or DEFAULT_ENDPOINT
|
||||||
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
|
|
||||||
return cache[token]
|
return cache[token]
|
||||||
|
|
||||||
|
|
||||||
@@ -89,25 +78,21 @@ def get_token():
|
|||||||
return request.cookies.get("token") or current_app.config["NUBES_API_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():
|
def get_client():
|
||||||
"""HttpClient с учётом режима и стенда.
|
"""HttpClient с учётом режима и стенда.
|
||||||
|
|
||||||
Эмуляция: polygon_url + "/" + stand → https://polygon.../{stand}/api/v1/svc
|
Эмуляция: polygon_url + "/" + stand → https://polygon.../{stand}/api/v1/svc
|
||||||
Облако: реальный API с автоопределением стенда (dev/test по токену)."""
|
Облако: реальный API с автоопределением стенда (dev/test/prod по токену)."""
|
||||||
token = get_token()
|
return HttpClient(get_endpoint(), 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)
|
|
||||||
|
|
||||||
|
|
||||||
def get_client_id():
|
def get_client_id():
|
||||||
@@ -139,16 +124,12 @@ def get_stand():
|
|||||||
"""Определить стенд.
|
"""Определить стенд.
|
||||||
|
|
||||||
Эмуляция: "polygon_{dev|test|prod}" — для load_service_ids и трекера.
|
Эмуляция: "polygon_{dev|test|prod}" — для load_service_ids и трекера.
|
||||||
Облако: автоопределение или stand_name (dev/test/mock)."""
|
Облако: автоопределение или stand_name (dev/test/prod/mock)."""
|
||||||
mode = get_mode()
|
mode = get_mode()
|
||||||
if mode == "polygon":
|
if mode == "polygon":
|
||||||
return f"polygon_{get_polygon_stand()}"
|
return f"polygon_{get_polygon_stand()}"
|
||||||
|
|
||||||
token = get_token()
|
endpoint = _cached_detect(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"
|
|
||||||
s = stand_name(endpoint)
|
s = stand_name(endpoint)
|
||||||
return s if s != "?" else "mock"
|
return s if s != "?" else "mock"
|
||||||
|
|
||||||
|
|||||||
+14
-10
@@ -11,7 +11,7 @@ HttpClient:
|
|||||||
|
|
||||||
Функции автоопределения стенда:
|
Функции автоопределения стенда:
|
||||||
- detect_endpoint(token) — пробует настроенные API стенды по токену
|
- detect_endpoint(token) — пробует настроенные API стенды по токену
|
||||||
- stand_name(endpoint) — "dev" / "test" по URL
|
- stand_name(endpoint) — возвращает имя стенда по URL
|
||||||
- create_client(token) — HttpClient + endpoint одним вызовом
|
- create_client(token) — HttpClient + endpoint одним вызовом
|
||||||
|
|
||||||
Зачем автоопределение: пользователь вводит токен, мы не знаем его стенд.
|
Зачем автоопределение: пользователь вводит токен, мы не знаем его стенд.
|
||||||
@@ -20,12 +20,16 @@ HttpClient:
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# Список стендов для автоопределения.
|
# Источник истины для URL облачных API. Меняй значения здесь и передеплой,
|
||||||
# Порядок ВАЖЕН: dev первый — он быстрее (меньше нагрузка), test — резервный.
|
# а не через NUBES_API_ENDPOINT в параметрах сервиса.
|
||||||
STANDS = [
|
API_ENDPOINTS = {
|
||||||
"https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
|
"dev": "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
|
||||||
"https://lk-api-gateway-test.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):
|
def detect_endpoint(token, endpoints=None):
|
||||||
@@ -54,8 +58,8 @@ def detect_endpoint(token, endpoints=None):
|
|||||||
def stand_name(endpoint):
|
def stand_name(endpoint):
|
||||||
"""Определить имя стенда по URL — точное совпадение endpoint.
|
"""Определить имя стенда по URL — точное совпадение endpoint.
|
||||||
|
|
||||||
Возвращает 'dev'/'test' или '?' если не совпало."""
|
Возвращает имя стенда или '?' если URL неизвестен."""
|
||||||
for name, url in (("dev", STANDS[0]), ("test", STANDS[1])):
|
for name, url in API_ENDPOINTS.items():
|
||||||
if (endpoint or "").rstrip("/") == url.rstrip("/"):
|
if (endpoint or "").rstrip("/") == url.rstrip("/"):
|
||||||
return name
|
return name
|
||||||
return "?"
|
return "?"
|
||||||
@@ -64,7 +68,7 @@ def stand_name(endpoint):
|
|||||||
def create_client(token, fallback_endpoint=None):
|
def create_client(token, fallback_endpoint=None):
|
||||||
"""HttpClient с автоопределением стенда.
|
"""HttpClient с автоопределением стенда.
|
||||||
|
|
||||||
Сначала detect_endpoint(token) — dev→test.
|
Сначала detect_endpoint(token) — dev→test→prod.
|
||||||
Если не сработало — fallback_endpoint (из конфига).
|
Если не сработало — fallback_endpoint (из конфига).
|
||||||
Возвращает (HttpClient, endpoint_url) или None."""
|
Возвращает (HttpClient, endpoint_url) или None."""
|
||||||
ep = detect_endpoint(token) or fallback_endpoint
|
ep = detect_endpoint(token) or fallback_endpoint
|
||||||
|
|||||||
+3
-8
@@ -16,8 +16,8 @@ Blueprint'ы (каждый — отдельный файл в routes/):
|
|||||||
gunicorn app:app --workers 2 --bind 0.0.0.0:8000
|
gunicorn app:app --workers 2 --bind 0.0.0.0:8000
|
||||||
|
|
||||||
Конфигурация:
|
Конфигурация:
|
||||||
NUBES_API_ENDPOINT — URL API (по умолчанию test)
|
NUBES_API_TOKEN — необязательный fallback-токен из переменных окружения
|
||||||
NUBES_API_TOKEN — сервисный токен (из переменных окружения k8s)
|
API_ENDPOINTS — URL DEV/TEST/PROD в api/http_client.py
|
||||||
VERSION — версия приложения (меняется при КАЖДОМ изменении кода)
|
VERSION — версия приложения (меняется при КАЖДОМ изменении кода)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -32,19 +32,14 @@ from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp
|
|||||||
|
|
||||||
# Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода.
|
# Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода.
|
||||||
# Нужна для фильтрации истории (пользователь видит только записи своей версии).
|
# Нужна для фильтрации истории (пользователь видит только записи своей версии).
|
||||||
VERSION = "1.2.49"
|
VERSION = "1.2.50"
|
||||||
|
|
||||||
# Flask-приложение с Jinja2-шаблонами из папки templates/
|
# Flask-приложение с Jinja2-шаблонами из папки templates/
|
||||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||||
|
|
||||||
# Конфигурация из переменных окружения (задаются в Nubes UI при создании сервиса)
|
# Конфигурация из переменных окружения (задаются в 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["NUBES_API_TOKEN"] = os.getenv("NUBES_API_TOKEN", "")
|
||||||
app.config["POLYGON_ENDPOINT"] = os.getenv("POLYGON_ENDPOINT", "") # если задан → все запросы в polygon
|
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
|
app.config["VERSION"] = VERSION
|
||||||
|
|
||||||
# Регистрируем blueprint'ы — каждый отвечает за свою группу маршрутов
|
# Регистрируем blueprint'ы — каждый отвечает за свою группу маршрутов
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
import threading
|
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
|
from runner import run_tests, get_status, load_config, save_config
|
||||||
|
|
||||||
bp = Blueprint("api", __name__)
|
bp = Blueprint("api", __name__)
|
||||||
@@ -10,11 +10,11 @@ bp = Blueprint("api", __name__)
|
|||||||
|
|
||||||
@bp.route("/api/run", methods=["POST"])
|
@bp.route("/api/run", methods=["POST"])
|
||||||
def api_run():
|
def api_run():
|
||||||
token = current_app.config["NUBES_API_TOKEN"]
|
token = get_token()
|
||||||
client = get_client()
|
client = get_client()
|
||||||
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
|
||||||
if not client:
|
if not client:
|
||||||
return jsonify({"error": "Не удалось определить стенд"}), 500
|
return jsonify({"error": "Не удалось определить стенд"}), 500
|
||||||
|
endpoint = get_endpoint()
|
||||||
t = threading.Thread(
|
t = threading.Thread(
|
||||||
target=run_tests,
|
target=run_tests,
|
||||||
args=(endpoint, token),
|
args=(endpoint, token),
|
||||||
|
|||||||
@@ -104,11 +104,6 @@ def run_tests(endpoint, token):
|
|||||||
endpoint: str — URL API
|
endpoint: str — URL API
|
||||||
token: str — JWT-токен"""
|
token: str — JWT-токен"""
|
||||||
global _current_run
|
global _current_run
|
||||||
# POLYGON_ENDPOINT проверяется в get_client(), здесь — прямой вызов для CLI
|
|
||||||
import os
|
|
||||||
polygon = os.getenv("POLYGON_ENDPOINT", "")
|
|
||||||
if polygon:
|
|
||||||
endpoint = polygon
|
|
||||||
client = HttpClient(endpoint, token)
|
client = HttpClient(endpoint, token)
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -2,6 +2,12 @@
|
|||||||
Тесты: auth.py — режимы, стенды, URL, cookie.
|
Тесты: auth.py — режимы, стенды, URL, cookie.
|
||||||
"""
|
"""
|
||||||
import api.auth as auth
|
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):
|
def test_polygon_url_construction(monkeypatch):
|
||||||
@@ -125,18 +131,15 @@ def test_whitelist_stand_invalid(monkeypatch):
|
|||||||
assert auth.get_polygon_stand() == "test" # fallback
|
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
|
from flask import Flask
|
||||||
|
|
||||||
prod_endpoint = "https://prod.example/api/v1/svc"
|
prod_endpoint = API_ENDPOINTS["prod"]
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config["NUBES_API_ENDPOINT"] = auth.STANDS[1]
|
|
||||||
app.config["NUBES_API_PROD_ENDPOINT"] = prod_endpoint
|
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
def fake_detect(token, endpoints):
|
def fake_detect(token):
|
||||||
captured["token"] = token
|
captured["token"] = token
|
||||||
captured["endpoints"] = endpoints
|
|
||||||
return prod_endpoint
|
return prod_endpoint
|
||||||
|
|
||||||
monkeypatch.setattr(auth, "detect_endpoint", fake_detect)
|
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 auth._cached_detect("prod-token") == prod_endpoint
|
||||||
|
|
||||||
assert captured["token"] == "prod-token"
|
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
|
from api import http_client
|
||||||
|
|
||||||
prod_endpoint = "https://prod.example/api/v1/svc"
|
prod_endpoint = API_ENDPOINTS["prod"]
|
||||||
endpoints = [*auth.STANDS, prod_endpoint]
|
endpoints = STANDS
|
||||||
attempted = []
|
attempted = []
|
||||||
|
|
||||||
class FakeHttpClient:
|
class FakeHttpClient:
|
||||||
@@ -171,13 +173,11 @@ def test_detect_endpoint_tries_configured_prod_endpoint(monkeypatch):
|
|||||||
assert attempted == endpoints
|
assert attempted == endpoints
|
||||||
|
|
||||||
|
|
||||||
def test_get_stand_labels_configured_prod_endpoint(monkeypatch):
|
def test_get_stand_labels_prod_endpoint(monkeypatch):
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
prod_endpoint = "https://prod.example/api/v1/svc"
|
prod_endpoint = API_ENDPOINTS["prod"]
|
||||||
app = Flask(__name__)
|
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_mode", lambda: "cloud")
|
||||||
monkeypatch.setattr(auth, "get_token", lambda: "prod-token")
|
monkeypatch.setattr(auth, "get_token", lambda: "prod-token")
|
||||||
monkeypatch.setattr(auth, "_cached_detect", lambda token: prod_endpoint)
|
monkeypatch.setattr(auth, "_cached_detect", lambda token: prod_endpoint)
|
||||||
|
|||||||
Reference in New Issue
Block a user