v0.5.0: 3 стенда — изоляция состояния, WSGI middleware, OpenAPI server selector, POLYGON_STANDS env
- loader.py: STANDS dict, get_services/get_ops_index per stand, POLYGON_STANDS env
- mock_state.py: get_state(stand_id) — ленивая инициализация
- app.py: StandMiddleware — перезапись PATH_INFO до Flask-роутинга
- Все роуты: LocalProxy(state), LocalProxy(SERVICES/OPS_INDEX) через g.stand_id
- openapi.py: _build_servers() — по одному серверу на стенд
- services/stand{1,2,3}/ — 37 YAML на стенд из STANDS/dev и STANDS/test
- tests/test_api.py: +4 multi-stand теста (изоляция, fail-next, OpenAPI servers)
- Обратная совместимость: /api/v1/svc/... → стенд по умолчанию
This commit is contained in:
@@ -10,10 +10,14 @@ Blueprint "instances" с префиксом /api/v1/svc/instances.
|
||||
HttpClient.post() в app-autotest достаёт instanceUid ИМЕННО из Location.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, jsonify, make_response, request
|
||||
from flask import Blueprint, g, jsonify, make_response, request
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
import mock_state
|
||||
from config.loader import SERVICES
|
||||
import config.loader as _cfg
|
||||
|
||||
state = LocalProxy(lambda: mock_state.get_state(g.stand_id))
|
||||
SERVICES = LocalProxy(lambda: _cfg.get_services(g.stand_id))
|
||||
|
||||
bp = Blueprint("instances", __name__, url_prefix="/api/v1/svc/instances")
|
||||
|
||||
@@ -32,7 +36,7 @@ def list_instances():
|
||||
"""
|
||||
page_size = request.args.get("pageSize", 200, type=int)
|
||||
page = request.args.get("page", 1, type=int)
|
||||
result = mock_state.state.list_instances(page_size=page_size, page=page)
|
||||
result = state.list_instances(page_size=page_size, page=page)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@@ -45,7 +49,7 @@ def get_instance(uid):
|
||||
|
||||
Если инстанс не найден — 404 с JSON-ошибкой.
|
||||
"""
|
||||
inst = mock_state.state.get_instance(uid)
|
||||
inst = state.get_instance(uid)
|
||||
if not inst:
|
||||
return jsonify({"error": "instance not found"}), 404
|
||||
|
||||
@@ -81,7 +85,7 @@ def create_instance():
|
||||
return jsonify({"error": f"service {service_id} not found"}), 404
|
||||
|
||||
# Создать shell инстанса (статус "creating")
|
||||
uid = mock_state.state.create_instance(service_id, display_name, svc_def)
|
||||
uid = state.create_instance(service_id, display_name, svc_def)
|
||||
|
||||
# ⛔ Location ОБЯЗАТЕЛЕН — без него executor не получит instanceUid
|
||||
resp = make_response(jsonify({"instanceUid": uid}), 201)
|
||||
|
||||
@@ -10,10 +10,14 @@ Blueprint "mock" с префиксом /api/v1/svc/_mock.
|
||||
|
||||
import os
|
||||
|
||||
from flask import Blueprint, jsonify, request, abort
|
||||
from flask import Blueprint, g, jsonify, request, abort
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
import mock_state
|
||||
import config.loader as _cfg # для чтения/записи модульных переменных DELAY, SERVICES
|
||||
import config.loader as _cfg
|
||||
|
||||
state = LocalProxy(lambda: mock_state.get_state(g.stand_id))
|
||||
SERVICES = LocalProxy(lambda: _cfg.get_services(g.stand_id))
|
||||
|
||||
bp = Blueprint("mock", __name__, url_prefix="/api/v1/svc/_mock")
|
||||
|
||||
@@ -44,7 +48,7 @@ def mock_reset():
|
||||
Удаляет ВСЕ инстансы, операции и параметры из MockState.
|
||||
Используется в тестах (autouse fixture) для изоляции тестов друг от друга.
|
||||
"""
|
||||
mock_state.state.reset()
|
||||
state.reset()
|
||||
return jsonify({"reset": "ok"})
|
||||
|
||||
|
||||
@@ -59,7 +63,7 @@ def mock_state_view():
|
||||
Полезно для отладки тестов: «какие инстансы сейчас живы?», «завершилась ли
|
||||
операция?». НЕ для production-использования.
|
||||
"""
|
||||
st = mock_state.state
|
||||
st = state
|
||||
return jsonify({
|
||||
"instances": {
|
||||
uid: {
|
||||
@@ -146,5 +150,5 @@ def mock_fail_next():
|
||||
После использования флаг сбрасывается (one-shot).
|
||||
Позволяет тестировать негативные сценарии.
|
||||
"""
|
||||
mock_state.state.fail_next = True
|
||||
state.fail_next = True
|
||||
return jsonify({"fail_next": True})
|
||||
|
||||
+15
-7
@@ -20,6 +20,17 @@ bp = Blueprint("openapi", __name__, url_prefix="/api/v1/svc")
|
||||
_SERVER_URL = os.getenv("POLYGON_ENDPOINT", "https://polygon.pythonk8s.dev.nubes.ru")
|
||||
|
||||
|
||||
def _build_servers():
|
||||
"""Список серверов — по одному на каждый стенд."""
|
||||
servers = []
|
||||
for sid in _cfg.list_stands():
|
||||
servers.append({
|
||||
"url": f"{_SERVER_URL}/{sid}",
|
||||
"description": f"Стенд: {sid}",
|
||||
})
|
||||
return servers
|
||||
|
||||
|
||||
@bp.route("/openapi.json", methods=["GET"])
|
||||
def openapi_spec():
|
||||
"""GET /api/v1/svc/openapi.json — OpenAPI 3.1.0 спецификация.
|
||||
@@ -56,18 +67,15 @@ def _build_spec():
|
||||
"- Управляемый сброс через `/_mock/reset`\n"
|
||||
"- Симуляция ошибок через `/_mock/fail-next`\n\n"
|
||||
"### Авторизация\n"
|
||||
f"Bearer-токен: `test-token-123` (настроен через `MOCK_AUTH_TOKEN`)"
|
||||
"- Основные эндпоинты — без авторизации\n"
|
||||
"- `_mock/*` — заголовок `X-Mock-Auth: test-token-123` "
|
||||
"(настроен через `MOCK_AUTH_TOKEN`)"
|
||||
),
|
||||
"contact": {
|
||||
"name": "Nubes Platform",
|
||||
},
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": _SERVER_URL,
|
||||
"description": "Polygon (mock)",
|
||||
},
|
||||
],
|
||||
"servers": _build_servers(),
|
||||
"security": [],
|
||||
"tags": [
|
||||
{"name": "services", "description": "Сервисы (каталог)"},
|
||||
|
||||
@@ -13,11 +13,16 @@ Blueprint "operations" с префиксом /api/v1/svc.
|
||||
выше для ясности.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, jsonify, make_response, request
|
||||
from flask import Blueprint, g, jsonify, make_response, request
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
import mock_state
|
||||
import state_machine
|
||||
from config.loader import SERVICES, OPS_INDEX
|
||||
import config.loader as _cfg
|
||||
|
||||
state = LocalProxy(lambda: mock_state.get_state(g.stand_id))
|
||||
SERVICES = LocalProxy(lambda: _cfg.get_services(g.stand_id))
|
||||
OPS_INDEX = LocalProxy(lambda: _cfg.get_ops_index(g.stand_id))
|
||||
|
||||
bp = Blueprint("operations", __name__, url_prefix="/api/v1/svc")
|
||||
|
||||
@@ -135,7 +140,7 @@ def create_operation():
|
||||
return jsonify({"error": "instanceUid is required"}), 400
|
||||
|
||||
# Проверка что инстанс существует
|
||||
inst = mock_state.state.get_instance(instance_uid)
|
||||
inst = state.get_instance(instance_uid)
|
||||
if not inst:
|
||||
return jsonify({"error": "instance not found"}), 404
|
||||
|
||||
@@ -165,7 +170,7 @@ def create_operation():
|
||||
break
|
||||
|
||||
# Создать операцию в MockState (dtFinish=None — ещё не выполнена)
|
||||
op_uid = mock_state.state.create_operation(instance_uid, svc_op_id, operation, kind, action)
|
||||
op_uid = state.create_operation(instance_uid, svc_op_id, operation, kind, action)
|
||||
|
||||
# ⛔ Location ОБЯЗАТЕЛЕН — без него executor не получит opUid
|
||||
resp = make_response(jsonify({"instanceOperationUid": op_uid}), 201)
|
||||
@@ -192,7 +197,7 @@ def get_operation(uid):
|
||||
app-autotest использует ?fields=dtFinish,isSuccessful для поллинга —
|
||||
ждёт пока dtFinish != None.
|
||||
"""
|
||||
op = mock_state.state.get_operation(uid)
|
||||
op = state.get_operation(uid)
|
||||
if not op:
|
||||
return jsonify({"error": "operation not found"}), 404
|
||||
|
||||
@@ -203,7 +208,7 @@ def get_operation(uid):
|
||||
if "cfsParams" in fields:
|
||||
svc_op_id = op.get("svcOperationId")
|
||||
svc_def = OPS_INDEX.get(svc_op_id, {})
|
||||
op_params = mock_state.state.get_params(uid)
|
||||
op_params = state.get_params(uid)
|
||||
result["cfsParams"] = _build_cfs_list(svc_def, svc_op_id, op_params)
|
||||
|
||||
return jsonify({"instanceOperation": result})
|
||||
@@ -233,11 +238,11 @@ def set_operation_param():
|
||||
return jsonify({"error": "instanceOperationUid and svcOperationCfsParamId required"}), 400
|
||||
|
||||
# Проверка что операция существует
|
||||
if not mock_state.state.get_operation(op_uid):
|
||||
if not state.get_operation(op_uid):
|
||||
return jsonify({"error": "operation not found"}), 404
|
||||
|
||||
# Сохранить параметр (param_id — int, value — str)
|
||||
mock_state.state.set_param(op_uid, param_id, value)
|
||||
state.set_param(op_uid, param_id, value)
|
||||
return jsonify({})
|
||||
|
||||
|
||||
@@ -256,7 +261,7 @@ def validate_cfs(uid):
|
||||
Если вернуть jsonify({}), app-autotest распарсит но логика та же.
|
||||
Пустое тело — канонический ответ реального Nubes API для validate-cfs.
|
||||
"""
|
||||
if not mock_state.state.get_operation(uid):
|
||||
if not state.get_operation(uid):
|
||||
return jsonify({"error": "operation not found"}), 404
|
||||
|
||||
return "", 200
|
||||
|
||||
+6
-3
@@ -7,12 +7,15 @@ Blueprint "root" регистрируется в app.py БЕЗ url_prefix.
|
||||
"""
|
||||
|
||||
import os
|
||||
from flask import Blueprint, render_template
|
||||
from flask import Blueprint, g, render_template
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
import mock_state
|
||||
import config.loader as _cfg
|
||||
|
||||
# Версия — из config/loader.py (единый источник правды)
|
||||
state = LocalProxy(lambda: mock_state.get_state(g.stand_id))
|
||||
SERVICES = LocalProxy(lambda: _cfg.get_services(g.stand_id))
|
||||
|
||||
VERSION = _cfg.VERSION
|
||||
|
||||
# Токен для авторизации в Swagger UI (предзаполняется)
|
||||
@@ -44,7 +47,7 @@ def index():
|
||||
Использует Jinja2-шаблон templates/index.html и внешний CSS из static/style.css.
|
||||
"""
|
||||
svc_count = len(_cfg.SERVICES)
|
||||
inst_count = len(mock_state.state.instances)
|
||||
inst_count = len(state.instances)
|
||||
|
||||
# Список сервисов для таблицы: name, id, кол-во операций
|
||||
svc_list = []
|
||||
|
||||
+7
-3
@@ -10,13 +10,17 @@ Blueprint "run" с префиксом /api/v1/svc.
|
||||
|
||||
import time
|
||||
|
||||
from flask import Blueprint, jsonify
|
||||
from flask import Blueprint, g, jsonify
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
import mock_state
|
||||
import state_machine
|
||||
import config.loader as _cfg
|
||||
from utils.now import now as _now
|
||||
|
||||
state = LocalProxy(lambda: mock_state.get_state(g.stand_id))
|
||||
SERVICES = LocalProxy(lambda: _cfg.get_services(g.stand_id))
|
||||
|
||||
bp = Blueprint("run", __name__, url_prefix="/api/v1/svc")
|
||||
|
||||
|
||||
@@ -37,7 +41,7 @@ def run_operation(uid):
|
||||
Возвращает {"ok": True} — формат как в реальном Nubes API.
|
||||
Повторный вызов → 409 (operation already completed).
|
||||
"""
|
||||
op = mock_state.state.get_operation(uid)
|
||||
op = state.get_operation(uid)
|
||||
if not op:
|
||||
return jsonify({"error": "operation not found"}), 404
|
||||
|
||||
@@ -55,7 +59,7 @@ def run_operation(uid):
|
||||
time.sleep(_cfg.DELAY)
|
||||
|
||||
# Проверка /_mock/fail-next — симуляция падения операции
|
||||
st = mock_state.state
|
||||
st = state
|
||||
if st.fail_next:
|
||||
st.fail_next = False # one-shot: сбросить после использования
|
||||
op["dtFinish"] = _now()
|
||||
|
||||
@@ -6,9 +6,12 @@ Blueprint "services" с префиксом /api/v1/svc/services.
|
||||
GET /services/<id> — детали конкретного сервиса (список операций)
|
||||
"""
|
||||
|
||||
from flask import Blueprint, jsonify
|
||||
from flask import Blueprint, g, jsonify
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from config.loader import SERVICES
|
||||
import config.loader as _cfg
|
||||
|
||||
SERVICES = LocalProxy(lambda: _cfg.get_services(g.stand_id))
|
||||
|
||||
bp = Blueprint("services", __name__, url_prefix="/api/v1/svc/services")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user