Files
polygon/site/routes/operations_routes.py
T
naeel 58d54f2a95 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/... → стенд по умолчанию
2026-08-01 20:10:19 +04:00

268 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
routes/operations_routes.py — эндпоинты операций (instanceOperations).
Blueprint "operations" с префиксом /api/v1/svc.
GET /instanceOperations/default/<int:op_id> — шаблон операции (cfsParams)
POST /instanceOperations — создать операцию → 201 + Location
GET /instanceOperations/<uid> — статус операции (+cfsParams)
POST /instanceOperationCfsParams — установить параметр
GET /instanceOperations/<uid>/validate-cfs — пустое тело, 200
Порядок маршрутов: default/<int:op_id> (целое число) и <uid> (строка UUID)
различаются Flask/Werkzeug автоматически — конфликта нет, но default размещён
выше для ясности.
"""
from flask import Blueprint, g, jsonify, make_response, request
from werkzeug.local import LocalProxy
import mock_state
import state_machine
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")
# ---------------------------------------------------------------------------
# Хелпер: сборка cfsParams-списка для операции
# ---------------------------------------------------------------------------
def _build_cfs_list(svc_def, op_id, op_params=None):
"""Собрать список cfsParams для заданной операции.
Используется в двух местах:
1. get_operation_default — шаблон БЕЗ paramValue
2. get_operation — статус операции С paramValue из op_params
Args:
svc_def — конфиг сервиса (из SERVICES или OPS_INDEX)
op_id — svcOperationId
op_params — {paramId: value} или None (если не нужны текущие значения)
Returns:
[{svcOperationCfsParamId, svcOperationCfsParam, dataType, ...,
paramValue?}]
"""
cfs_params_by_op = svc_def.get("cfsParamsByOp", {})
cfs_params = {p["svcOperationCfsParamId"]: p for p in svc_def.get("cfsParams", [])}
param_ids = cfs_params_by_op.get(op_id, [])
cfs_list = []
for pid in param_ids:
p = cfs_params.get(pid)
if p:
entry = dict(p)
# Если переданы op_params — добавить текущее значение параметра
if op_params is not None:
entry["paramValue"] = op_params.get(pid, "")
cfs_list.append(entry)
return cfs_list
# ---------------------------------------------------------------------------
# GET /instanceOperations/default/<int:op_id>
# ---------------------------------------------------------------------------
@bp.route("/instanceOperations/default/<int:op_id>", methods=["GET"])
def get_operation_default(op_id):
"""GET /instanceOperations/default/{id} — шаблон операции.
КЛЮЧЕВОЙ эндпоинт для app-autotest. Именно отсюда executor берёт список
параметров (cfsParams) с:
- dataDescriptor (подполя map-fixed: cpu, memory, replicas, ...)
- valueList (допустимые значения для select/дропдаунов)
- isModifiable (какие параметры можно менять при modify)
- isRequired, defaultValue
Без этого эндпоинта app-autotest не сможет отрендерить форму параметров.
Если op_id не найден ни в одном сервисе — 404.
"""
# Ищем сервис по ID операции через OPS_INDEX (построен в config/loader.py)
svc_def = OPS_INDEX.get(op_id)
if not svc_def:
return jsonify({"error": "operation not found"}), 404
# Найти операцию в списке операций сервиса
target_op = None
for op in svc_def.get("operations", []):
if op["svcOperationId"] == op_id:
target_op = op
break
if not target_op:
return jsonify({"error": "operation not found"}), 404
# Собрать cfsParams без paramValue (шаблон)
cfs_list = _build_cfs_list(svc_def, op_id)
return jsonify({
"svcOperation": {
"svcOperationId": op_id,
"operation": target_op.get("operation", ""),
"cfsParams": cfs_list,
}
})
# ---------------------------------------------------------------------------
# POST /instanceOperations
# ---------------------------------------------------------------------------
@bp.route("/instanceOperations", methods=["POST"])
def create_operation():
"""POST /instanceOperations — создать операцию для инстанса.
Ожидает JSON: {instanceUid: str, operation: str, svcOperationId?: int}
Особый случай: когда operation="create", app-autotest НЕ передаёт
svcOperationId (потому что на этом этапе executor ещё не знает ID
create-операции). Polygon находит svcOperationId сам через service_def.
⛔ КРИТИЧНО: возвращает 201 + заголовок Location: ./{opUid}.
HttpClient.post() достаёт instanceOperationUid из Location.
Также заполняет kind и action операции из конфига сервиса — они нужны
state_machine.apply_effect() чтобы понять что делать при run.
"""
body = request.get_json(silent=True) or {}
instance_uid = body.get("instanceUid")
operation = body.get("operation", "")
svc_op_id = body.get("svcOperationId")
# Валидация: instanceUid обязателен
if not instance_uid:
return jsonify({"error": "instanceUid is required"}), 400
# Проверка что инстанс существует
inst = state.get_instance(instance_uid)
if not inst:
return jsonify({"error": "instance not found"}), 404
# Если svcOperationId не передан — найти по имени операции
# (например app-autotest при create не знает ID, передаёт только "create")
if svc_op_id is None:
svc_id = inst.get("serviceId")
svc_def = SERVICES.get(svc_id, {})
for op in svc_def.get("operations", []):
if op["operation"] == operation:
svc_op_id = op["svcOperationId"]
break
# Если и после поиска не нашли — операция не поддерживается сервисом
if svc_op_id is None:
return jsonify({"error": f"operation '{operation}' not found for service"}), 404
# Определить kind и action операции из конфига
# (один проход — поиск по svcOperationId, не по имени)
svc_def = OPS_INDEX.get(svc_op_id, {})
kind = "instance"
action = operation
for op in svc_def.get("operations", []):
if op["svcOperationId"] == svc_op_id:
kind = op.get("kind", "instance")
action = op.get("action", operation)
break
# Создать операцию в MockState (dtFinish=None — ещё не выполнена)
op_uid = state.create_operation(instance_uid, svc_op_id, operation, kind, action)
# ⛔ Location ОБЯЗАТЕЛЕН — без него executor не получит opUid
resp = make_response(jsonify({"instanceOperationUid": op_uid}), 201)
resp.headers["Location"] = f"./{op_uid}"
return resp
# ---------------------------------------------------------------------------
# GET /instanceOperations/<uid>
# ---------------------------------------------------------------------------
@bp.route("/instanceOperations/<uid>", methods=["GET"])
def get_operation(uid):
"""GET /instanceOperations/{uid} — статус операции.
Query-параметры:
?fields=cfsParams,... — если содержит "cfsParams", добавляет в ответ
список параметров с ТЕКУЩИМИ paramValue.
Возвращает {instanceOperation: {instanceOperationUid, instanceUid,
svcOperationId, operation, kind, action, dtStart, dtFinish, isSuccessful,
errorLog, svc, stages, cfsParams?}}
app-autotest использует ?fields=dtFinish,isSuccessful для поллинга —
ждёт пока dtFinish != None.
"""
op = state.get_operation(uid)
if not op:
return jsonify({"error": "operation not found"}), 404
result = dict(op)
# Если клиент запросил cfsParams — добавить их с текущими paramValue
fields = request.args.get("fields", "")
if "cfsParams" in fields:
svc_op_id = op.get("svcOperationId")
svc_def = OPS_INDEX.get(svc_op_id, {})
op_params = state.get_params(uid)
result["cfsParams"] = _build_cfs_list(svc_def, svc_op_id, op_params)
return jsonify({"instanceOperation": result})
# ---------------------------------------------------------------------------
# POST /instanceOperationCfsParams
# ---------------------------------------------------------------------------
@bp.route("/instanceOperationCfsParams", methods=["POST"])
def set_operation_param():
"""POST /instanceOperationCfsParams — установить значение параметра операции.
Ожидает JSON: {instanceOperationUid, svcOperationCfsParamId, paramValue}
Вызывается N раз (по одному на каждый параметр) перед /run.
Значения сохраняются в mock_state.op_params[opUid][paramId] = value.
При apply_effect (modify) значения мержатся в state.params инстанса.
"""
body = request.get_json(silent=True) or {}
op_uid = body.get("instanceOperationUid")
param_id = body.get("svcOperationCfsParamId")
value = body.get("paramValue", "")
# Валидация: оба поля обязательны
if not op_uid or param_id is None:
return jsonify({"error": "instanceOperationUid and svcOperationCfsParamId required"}), 400
# Проверка что операция существует
if not state.get_operation(op_uid):
return jsonify({"error": "operation not found"}), 404
# Сохранить параметр (param_id — int, value — str)
state.set_param(op_uid, param_id, value)
return jsonify({})
# ---------------------------------------------------------------------------
# GET /instanceOperations/<uid>/validate-cfs
# ---------------------------------------------------------------------------
@bp.route("/instanceOperations/<uid>/validate-cfs", methods=["GET"])
def validate_cfs(uid):
"""GET /instanceOperations/{uid}/validate-cfs — валидация параметров.
⛔ КРИТИЧНО: возвращает ПУСТОЕ тело с кодом 200.
НЕ использовать jsonify() — app-autotest делает requests.get().json()
и ловит JSONDecodeError, считая пустой ответ = успех.
Если вернуть jsonify({}), app-autotest распарсит но логика та же.
Пустое тело — канонический ответ реального Nubes API для validate-cfs.
"""
if not state.get_operation(uid):
return jsonify({"error": "operation not found"}), 404
return "", 200