feat: support configured production API endpoint
This commit is contained in:
@@ -33,6 +33,8 @@ Flask-приложение для автотестов сервисов Nubes.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Для PROD автоопределения дополнительно укажи `NUBES_API_PROD_ENDPOINT` с подтверждённым URL PROD API. Значение не задаётся по умолчанию; без него доступны только DEV/TEST из списка в `site/api/http_client.py`.
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+15
-6
@@ -41,9 +41,17 @@ 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"]
|
endpoint = current_app.config["NUBES_API_ENDPOINT"].rstrip("/")
|
||||||
if endpoint in STANDS:
|
endpoints = [url.rstrip("/") for url in STANDS]
|
||||||
endpoint = detect_endpoint(token) or endpoint
|
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] = endpoint
|
||||||
return cache[token]
|
return cache[token]
|
||||||
|
|
||||||
@@ -137,11 +145,12 @@ def get_stand():
|
|||||||
return f"polygon_{get_polygon_stand()}"
|
return f"polygon_{get_polygon_stand()}"
|
||||||
|
|
||||||
token = get_token()
|
token = get_token()
|
||||||
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
endpoint = _cached_detect(token).rstrip("/")
|
||||||
if endpoint not in STANDS:
|
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"
|
||||||
return stand_name(_cached_detect(token))
|
|
||||||
|
|
||||||
|
|
||||||
def get_token_info():
|
def get_token_info():
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ HttpClient:
|
|||||||
- raw_delete: DELETE без авторизации (для CMDB)
|
- raw_delete: DELETE без авторизации (для CMDB)
|
||||||
|
|
||||||
Функции автоопределения стенда:
|
Функции автоопределения стенда:
|
||||||
- detect_endpoint(token) — пробует dev→test стенды по токену
|
- detect_endpoint(token) — пробует настроенные API стенды по токену
|
||||||
- stand_name(endpoint) — "dev" / "test" по URL
|
- stand_name(endpoint) — "dev" / "test" по URL
|
||||||
- create_client(token) — HttpClient + endpoint одним вызовом
|
- create_client(token) — HttpClient + endpoint одним вызовом
|
||||||
|
|
||||||
Зачем автоопределение: пользователь вводит токен, мы не знаем dev это или test.
|
Зачем автоопределение: пользователь вводит токен, мы не знаем его стенд.
|
||||||
Пробуем оба стенда — какой ответит с results != None, тот и рабочий.
|
Пробуем настроенные URL — какой ответит с results != None, тот и рабочий.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -28,8 +28,8 @@ STANDS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def detect_endpoint(token):
|
def detect_endpoint(token, endpoints=None):
|
||||||
"""Пробуем токен против dev и test стендов, возвращаем рабочий URL.
|
"""Пробуем токен против заданных API стендов, возвращаем рабочий URL.
|
||||||
|
|
||||||
Алгоритм:
|
Алгоритм:
|
||||||
1. Для каждого стенда создаём HttpClient + делаем GET /instances?pageSize=1.
|
1. Для каждого стенда создаём HttpClient + делаем GET /instances?pageSize=1.
|
||||||
@@ -40,7 +40,7 @@ def detect_endpoint(token):
|
|||||||
Почему results != None а не просто HTTP 200:
|
Почему results != None а не просто HTTP 200:
|
||||||
API может вернуть 200 с пустым списком (нет инстансов) — это норм.
|
API может вернуть 200 с пустым списком (нет инстансов) — это норм.
|
||||||
results=None — признак что ответ не соответствует ожидаемой структуре."""
|
results=None — признак что ответ не соответствует ожидаемой структуре."""
|
||||||
for ep in STANDS:
|
for ep in endpoints or STANDS:
|
||||||
try:
|
try:
|
||||||
c = HttpClient(ep, token)
|
c = HttpClient(ep, token)
|
||||||
data = c.get("/instances", params={"pageSize": 1, "page": 1})
|
data = c.get("/instances", params={"pageSize": 1, "page": 1})
|
||||||
|
|||||||
+2
-1
@@ -32,7 +32,7 @@ from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp
|
|||||||
|
|
||||||
# Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода.
|
# Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода.
|
||||||
# Нужна для фильтрации истории (пользователь видит только записи своей версии).
|
# Нужна для фильтрации истории (пользователь видит только записи своей версии).
|
||||||
VERSION = "1.2.48"
|
VERSION = "1.2.49"
|
||||||
|
|
||||||
# Flask-приложение с Jinja2-шаблонами из папки templates/
|
# Flask-приложение с Jinja2-шаблонами из папки templates/
|
||||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||||
@@ -44,6 +44,7 @@ app.config["NUBES_API_ENDPOINT"] = os.getenv(
|
|||||||
)
|
)
|
||||||
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'ы — каждый отвечает за свою группу маршрутов
|
||||||
|
|||||||
@@ -123,3 +123,64 @@ 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):
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
prod_endpoint = "https://prod.example/api/v1/svc"
|
||||||
|
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):
|
||||||
|
captured["token"] = token
|
||||||
|
captured["endpoints"] = endpoints
|
||||||
|
return prod_endpoint
|
||||||
|
|
||||||
|
monkeypatch.setattr(auth, "detect_endpoint", fake_detect)
|
||||||
|
monkeypatch.setattr(auth, "current_app", app)
|
||||||
|
with app.test_request_context("/"):
|
||||||
|
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):
|
||||||
|
from api import http_client
|
||||||
|
|
||||||
|
prod_endpoint = "https://prod.example/api/v1/svc"
|
||||||
|
endpoints = [*auth.STANDS, prod_endpoint]
|
||||||
|
attempted = []
|
||||||
|
|
||||||
|
class FakeHttpClient:
|
||||||
|
def __init__(self, endpoint, token):
|
||||||
|
self.endpoint = endpoint
|
||||||
|
|
||||||
|
def get(self, path, **kwargs):
|
||||||
|
attempted.append(self.endpoint)
|
||||||
|
if self.endpoint != prod_endpoint:
|
||||||
|
raise RuntimeError("token belongs to another stand")
|
||||||
|
return {"results": []}
|
||||||
|
|
||||||
|
monkeypatch.setattr(http_client, "HttpClient", FakeHttpClient)
|
||||||
|
|
||||||
|
assert http_client.detect_endpoint("prod-token", endpoints=endpoints) == prod_endpoint
|
||||||
|
assert attempted == endpoints
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_stand_labels_configured_prod_endpoint(monkeypatch):
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
prod_endpoint = "https://prod.example/api/v1/svc"
|
||||||
|
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)
|
||||||
|
monkeypatch.setattr(auth, "current_app", app)
|
||||||
|
|
||||||
|
assert auth.get_stand() == "prod"
|
||||||
|
|||||||
Reference in New Issue
Block a user