- 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/... → стенд по умолчанию
94 lines
3.8 KiB
Python
94 lines
3.8 KiB
Python
"""
|
||
routes/instances_routes.py — эндпоинты инстансов.
|
||
|
||
Blueprint "instances" с префиксом /api/v1/svc/instances.
|
||
GET /instances — список с пагинацией
|
||
GET /instances/<uid> — полные данные инстанса (state.params + state.out)
|
||
POST /instances — создать shell инстанса → 201 + Location: ./{uid}
|
||
|
||
КРИТИЧНО: POST /instances ОБЯЗАН возвращать заголовок Location.
|
||
HttpClient.post() в app-autotest достаёт instanceUid ИМЕННО из Location.
|
||
"""
|
||
|
||
from flask import Blueprint, g, jsonify, make_response, request
|
||
from werkzeug.local import LocalProxy
|
||
|
||
import mock_state
|
||
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")
|
||
|
||
|
||
@bp.route("", methods=["GET"])
|
||
@bp.route("/", methods=["GET"])
|
||
def list_instances():
|
||
"""GET /api/v1/svc/instances — список инстансов с пагинацией.
|
||
|
||
Query-параметры:
|
||
pageSize — размер страницы (по умолчанию 200, максимум 200)
|
||
page — номер страницы (по умолчанию 1)
|
||
|
||
Возвращает {results, pageSize, page, total}.
|
||
Пагинация: стоп по len(batch) < pageSize (как в реальном API).
|
||
"""
|
||
page_size = request.args.get("pageSize", 200, type=int)
|
||
page = request.args.get("page", 1, type=int)
|
||
result = state.list_instances(page_size=page_size, page=page)
|
||
return jsonify(result)
|
||
|
||
|
||
@bp.route("/<uid>", methods=["GET"])
|
||
def get_instance(uid):
|
||
"""GET /api/v1/svc/instances/{uid} — полные данные инстанса.
|
||
|
||
Возвращает {instance: {instanceUid, serviceId, displayName, status,
|
||
explainedStatus, svc, dtCreate, state: {params: {...}, out: {...}}}}.
|
||
|
||
Если инстанс не найден — 404 с JSON-ошибкой.
|
||
"""
|
||
inst = state.get_instance(uid)
|
||
if not inst:
|
||
return jsonify({"error": "instance not found"}), 404
|
||
|
||
return jsonify({"instance": dict(inst)})
|
||
|
||
|
||
@bp.route("", methods=["POST"])
|
||
@bp.route("/", methods=["POST"])
|
||
def create_instance():
|
||
"""POST /api/v1/svc/instances — создать shell инстанса.
|
||
|
||
Ожидает JSON: {serviceId: int, displayName: str?}
|
||
|
||
⛔ КРИТИЧНО: возвращает 201 + заголовок Location: ./{instanceUid}.
|
||
HttpClient.post() в app-autotest парсит Location чтобы получить UUID:
|
||
- Берёт последний сегмент после /
|
||
- Проверяет len(сегмент) >= 32 (длина UUID без дефисов)
|
||
|
||
Также возвращает {"instanceUid": uid} в теле — двойная защита
|
||
на случай если заголовок не дойдёт.
|
||
"""
|
||
body = request.get_json(silent=True) or {}
|
||
service_id = body.get("serviceId")
|
||
display_name = body.get("displayName", "unnamed")
|
||
|
||
# Валидация: serviceId обязателен
|
||
if service_id is None:
|
||
return jsonify({"error": "serviceId is required"}), 400
|
||
|
||
# Проверка что сервис существует в конфиге
|
||
svc_def = SERVICES.get(service_id)
|
||
if not svc_def:
|
||
return jsonify({"error": f"service {service_id} not found"}), 404
|
||
|
||
# Создать shell инстанса (статус "creating")
|
||
uid = state.create_instance(service_id, display_name, svc_def)
|
||
|
||
# ⛔ Location ОБЯЗАТЕЛЕН — без него executor не получит instanceUid
|
||
resp = make_response(jsonify({"instanceUid": uid}), 201)
|
||
resp.headers["Location"] = f"./{uid}"
|
||
return resp
|