Files
polygon/site/routes/openapi.py
T
naeel ef593101a3 feat: Swagger UI + OpenAPI 3.1.0 спека (v0.4.0)
- /swagger — Swagger UI (CDN, без pip-зависимостей)
- /api/v1/svc/openapi.json — динамическая OpenAPI 3.1.0 спека
- Nubes-брендированный topbar (лого + версия)
- Bearer-токен предзаполняется автоматически
- 17 эндпоинтов с полными схемами, примерами, описаниями
- Ссылка «Swagger» на главной странице
2026-08-01 09:41:55 +04:00

790 lines
38 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/openapi.py — OpenAPI 3.1.0 спецификация.
Blueprint "openapi" с префиксом /api/v1/svc.
GET /openapi.json — динамическая OpenAPI-спека, построенная из загруженных сервисов.
Swagger UI загружает этот JSON через /swagger и рендерит интерактивную
документацию. Спека генерируется при каждом запросе — всегда актуальна.
"""
import os
from flask import Blueprint, jsonify
import config.loader as _cfg
bp = Blueprint("openapi", __name__, url_prefix="/api/v1/svc")
# URL для сервера — из env или автоопределение
_SERVER_URL = os.getenv("POLYGON_ENDPOINT", "https://polygon.pythonk8s.dev.nubes.ru")
@bp.route("/openapi.json", methods=["GET"])
def openapi_spec():
"""GET /api/v1/svc/openapi.json — OpenAPI 3.1.0 спецификация.
Возвращает полную спецификацию всех эндпоинтов полигона.
Динамически добавляет схемы параметров для каждого загруженного сервиса.
"""
return jsonify(_build_spec())
def _build_spec():
"""Собрать OpenAPI 3.1.0 спецификацию.
Включает:
- Все 17 эндпоинтов (REST API + _mock)
- Bearer-авторизацию
- Схемы: Service, Instance, Operation, CfsParam, Stage
- Примеры запросов и ответов
"""
return {
"openapi": "3.1.0",
"info": {
"title": "Polygon — Mock Nubes API",
"version": _cfg.VERSION,
"description": (
"Эмулятор REST API облачной платформы **Nubes** для интеграционных тестов.\n\n"
"Polygon полностью повторяет контракты реального API: сервисы, инстансы, "
"операции, параметры, валидацию — но работает **без реальной инфраструктуры**, "
"в памяти, с мгновенным откликом.\n\n"
"### Особенности\n"
"- Все операции синхронные (настраиваемая задержка через `/_mock/delay`)\n"
"- Состояние в памяти (1 gunicorn-воркер)\n"
"- Конфигурация сервисов из YAML (генерируется из STANDS)\n"
"- Управляемый сброс через `/_mock/reset`\n"
"- Симуляция ошибок через `/_mock/fail-next`\n\n"
"### Авторизация\n"
f"Bearer-токен: `test-token-123` (настроен через `MOCK_AUTH_TOKEN`)"
),
"contact": {
"name": "Nubes Platform",
},
},
"servers": [
{
"url": _SERVER_URL,
"description": "Polygon (mock)",
},
],
"security": [
{"bearerAuth": []},
],
"tags": [
{"name": "services", "description": "Сервисы (каталог)"},
{"name": "instances", "description": "Инстансы (CRUD)"},
{"name": "operations", "description": "Операции (create/modify/delete)"},
{"name": "mock", "description": "Служебные (сброс, отладка, задержка)"},
{"name": "health", "description": "Healthcheck"},
],
"paths": _build_paths(),
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "token",
"description": "MOCK_AUTH_TOKEN (по умолчанию: test-token-123)",
},
},
"schemas": {
"Service": {
"type": "object",
"properties": {
"svcId": {"type": "integer", "description": "Числовой ID сервиса"},
"svc": {"type": "string", "description": "Человекочитаемое имя (Болванка, PostgreSQL, …)"},
"svcShort": {"type": "string", "description": "Короткое имя (dummy, postgres, …)"},
},
},
"ServiceList": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {"$ref": "#/components/schemas/Service"},
},
},
},
"ServiceDetail": {
"type": "object",
"properties": {
"svc": {
"type": "object",
"properties": {
"svc": {"type": "string"},
"svcShort": {"type": "string"},
"operations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"svcOperationId": {"type": "integer", "description": "ID операции"},
"operation": {"type": "string", "description": "create | modify | delete | suspend | …"},
},
},
},
},
},
},
},
"InstanceListItem": {
"type": "object",
"properties": {
"instanceUid": {"type": "string", "format": "uuid", "description": "UUID инстанса"},
"serviceId": {"type": "integer", "description": "ID сервиса"},
"displayName": {"type": "string", "description": "Отображаемое имя"},
"svc": {"type": "string", "description": "Название сервиса"},
"explainedStatus": {"type": "string", "description": "Статус (Создание, Работает, …)"},
},
},
"InstanceList": {
"type": "object",
"properties": {
"results": {"type": "array", "items": {"$ref": "#/components/schemas/InstanceListItem"}},
"pageSize": {"type": "integer"},
"page": {"type": "integer"},
"total": {"type": "integer"},
},
},
"Instance": {
"type": "object",
"properties": {
"instanceUid": {"type": "string", "format": "uuid"},
"serviceId": {"type": "integer"},
"displayName": {"type": "string"},
"descr": {"type": "string"},
"status": {"type": "string", "description": "creating | running | modifying | deleting | suspended"},
"explainedStatus": {"type": "string"},
"svc": {"type": "string"},
"dtCreate": {"type": "string", "format": "date-time"},
"state": {
"type": "object",
"properties": {
"params": {"type": "object", "description": "Параметры инстанса"},
"out": {"type": "object", "description": "Output-параметры"},
},
},
},
},
"InstanceResponse": {
"type": "object",
"properties": {
"instance": {"$ref": "#/components/schemas/Instance"},
},
},
"CreateInstanceRequest": {
"type": "object",
"required": ["serviceId"],
"properties": {
"serviceId": {"type": "integer", "description": "ID сервиса из каталога"},
"displayName": {"type": "string", "description": "Отображаемое имя (по умолчанию: unnamed)"},
},
},
"CreateInstanceResponse": {
"type": "object",
"properties": {
"instanceUid": {"type": "string", "format": "uuid"},
},
},
"CfsParam": {
"type": "object",
"properties": {
"svcOperationCfsParamId": {"type": "integer", "description": "ID параметра"},
"svcOperationCfsParam": {"type": "string", "description": "Название параметра"},
"dataType": {"type": "string", "description": "string | integer | map | map-fixed | boolean"},
"dataDescriptor": {"type": "string", "description": "JSON-описание подполей (для map-типов)"},
"isRequired": {"type": "boolean"},
"isModifiable": {"type": "boolean"},
"defaultValue": {"type": "string"},
"paramValue": {"type": "string", "description": "Текущее значение (если задано)"},
"valueList": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {"type": "string"},
"label": {"type": "string"},
},
},
},
},
},
"OperationDefault": {
"type": "object",
"properties": {
"svcOperation": {
"type": "object",
"properties": {
"svcOperationId": {"type": "integer"},
"operation": {"type": "string"},
"cfsParams": {
"type": "array",
"items": {"$ref": "#/components/schemas/CfsParam"},
},
},
},
},
},
"Operation": {
"type": "object",
"properties": {
"instanceOperationUid": {"type": "string", "format": "uuid"},
"instanceUid": {"type": "string", "format": "uuid"},
"svcOperationId": {"type": "integer"},
"operation": {"type": "string"},
"kind": {"type": "string", "description": "instance | access"},
"action": {"type": "string", "description": "create | modify | delete | …"},
"dtStart": {"type": "string", "format": "date-time"},
"dtFinish": {"type": "string", "format": "date-time", "nullable": True},
"isSuccessful": {"type": "boolean"},
"errorLog": {"type": "string"},
"svc": {"type": "string"},
"stages": {"type": "array", "items": {"$ref": "#/components/schemas/Stage"}},
"cfsParams": {"type": "array", "items": {"$ref": "#/components/schemas/CfsParam"}},
},
},
"OperationResponse": {
"type": "object",
"properties": {
"instanceOperation": {"$ref": "#/components/schemas/Operation"},
},
},
"Stage": {
"type": "object",
"properties": {
"stage": {"type": "string"},
"status": {"type": "string", "description": "running | done | failed"},
"error": {"type": "string"},
"dtStart": {"type": "string", "format": "date-time"},
"dtFinish": {"type": "string", "format": "date-time"},
},
},
"CreateOperationRequest": {
"type": "object",
"required": ["instanceUid", "operation"],
"properties": {
"instanceUid": {"type": "string", "format": "uuid"},
"operation": {"type": "string", "description": "create | modify | delete | …"},
"svcOperationId": {"type": "integer", "description": "ID операции (опционально при create)"},
},
},
"CreateOperationResponse": {
"type": "object",
"properties": {
"instanceOperationUid": {"type": "string", "format": "uuid"},
},
},
"SetParamRequest": {
"type": "object",
"required": ["instanceOperationUid", "svcOperationCfsParamId"],
"properties": {
"instanceOperationUid": {"type": "string", "format": "uuid"},
"svcOperationCfsParamId": {"type": "integer"},
"paramValue": {"type": "string"},
},
},
"RunResponse": {
"type": "object",
"properties": {
"ok": {"type": "boolean"},
},
},
"MockState": {
"type": "object",
"properties": {
"instances": {"type": "object", "description": "uid → {instanceUid, displayName, status, serviceId}"},
"operations": {"type": "object", "description": "uid → {instanceOperationUid, instanceUid, operation, dtFinish, isSuccessful}"},
},
},
"MockServices": {
"type": "object",
"properties": {
"count": {"type": "integer"},
"services": {"type": "object", "description": "svcId → {name, displayName, operations, params}"},
},
},
"Error": {
"type": "object",
"properties": {
"error": {"type": "string"},
},
},
},
},
}
def _build_paths():
"""Собрать все paths (эндпоинты) спецификации."""
return {
# ── Health ──
"/health": {
"get": {
"tags": ["health"],
"summary": "Healthcheck",
"description": "Проверка живучести сервиса. Nubes дёргает этот эндпоинт периодически.",
"operationId": "health",
"security": [],
"responses": {
"200": {
"description": "OK",
"content": {"text/plain": {"schema": {"type": "string", "example": "OK"}}},
},
},
},
},
# ── Services ──
"/api/v1/svc/services": {
"get": {
"tags": ["services"],
"summary": "Список сервисов",
"description": "Возвращает все загруженные сервисы (каталог).",
"operationId": "listServices",
"responses": {
"200": {
"description": "Список сервисов",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ServiceList"}}},
},
},
},
},
"/api/v1/svc/services/{serviceId}": {
"get": {
"tags": ["services"],
"summary": "Детали сервиса",
"description": "Возвращает детали сервиса: название, список операций.",
"operationId": "getService",
"parameters": [
{
"name": "serviceId",
"in": "path",
"required": True,
"schema": {"type": "integer"},
"description": "ID сервиса",
},
],
"responses": {
"200": {
"description": "Детали сервиса",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ServiceDetail"}}},
},
"404": {
"description": "Сервис не найден",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
},
},
},
# ── Instances ──
"/api/v1/svc/instances": {
"get": {
"tags": ["instances"],
"summary": "Список инстансов",
"description": "Возвращает список инстансов с пагинацией.",
"operationId": "listInstances",
"parameters": [
{
"name": "pageSize",
"in": "query",
"schema": {"type": "integer", "default": 200, "maximum": 200},
"description": "Размер страницы (макс. 200)",
},
{
"name": "page",
"in": "query",
"schema": {"type": "integer", "default": 1},
"description": "Номер страницы",
},
],
"responses": {
"200": {
"description": "Список инстансов",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/InstanceList"}}},
},
},
},
"post": {
"tags": ["instances"],
"summary": "Создать инстанс",
"description": "Создаёт shell инстанса (статус: creating). Возвращает 201 + Location.",
"operationId": "createInstance",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/CreateInstanceRequest"},
"example": {"serviceId": 38, "displayName": "my-test-instance"},
},
},
},
"responses": {
"201": {
"description": "Инстанс создан",
"headers": {
"Location": {
"description": "Относительный URL инстанса (./{instanceUid})",
"schema": {"type": "string"},
},
},
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/CreateInstanceResponse"}}},
},
"400": {
"description": "serviceId обязателен",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
"404": {
"description": "Сервис не найден",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
},
},
},
"/api/v1/svc/instances/{instanceUid}": {
"get": {
"tags": ["instances"],
"summary": "Детали инстанса",
"description": "Возвращает полные данные инстанса: статус, параметры, output.",
"operationId": "getInstance",
"parameters": [
{
"name": "instanceUid",
"in": "path",
"required": True,
"schema": {"type": "string", "format": "uuid"},
"description": "UUID инстанса",
},
],
"responses": {
"200": {
"description": "Детали инстанса",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/InstanceResponse"}}},
},
"404": {
"description": "Инстанс не найден",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
},
},
},
# ── Operations: default ──
"/api/v1/svc/instanceOperations/default/{svcOperationId}": {
"get": {
"tags": ["operations"],
"summary": "Шаблон операции",
"description": (
"Возвращает шаблон операции: название, список cfsParams "
"(с dataDescriptor, valueList, isModifiable, isRequired).\n\n"
"**Ключевой эндпоинт** для app-autotest — отсюда executor берёт "
"структуру параметров для рендеринга формы."
),
"operationId": "getOperationDefault",
"parameters": [
{
"name": "svcOperationId",
"in": "path",
"required": True,
"schema": {"type": "integer"},
"description": "ID операции (svcOperationId)",
},
],
"responses": {
"200": {
"description": "Шаблон операции",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/OperationDefault"}}},
},
"404": {
"description": "Операция не найдена",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
},
},
},
# ── Operations: create ──
"/api/v1/svc/instanceOperations": {
"post": {
"tags": ["operations"],
"summary": "Создать операцию",
"description": (
"Создаёт операцию для инстанса. Возвращает 201 + Location.\n\n"
"При `operation=\"create\"` можно не передавать `svcOperationId` — "
"полигон найдёт его сам по сервису инстанса."
),
"operationId": "createOperation",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/CreateOperationRequest"},
"example": {
"instanceUid": "550e8400-e29b-41d4-a716-446655440000",
"operation": "create",
},
},
},
},
"responses": {
"201": {
"description": "Операция создана",
"headers": {
"Location": {
"description": "Относительный URL операции (./{instanceOperationUid})",
"schema": {"type": "string"},
},
},
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/CreateOperationResponse"}}},
},
"400": {
"description": "instanceUid обязателен",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
"404": {
"description": "Инстанс или операция не найдены",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
},
},
},
# ── Operations: detail + status ──
"/api/v1/svc/instanceOperations/{instanceOperationUid}": {
"get": {
"tags": ["operations"],
"summary": "Статус операции",
"description": (
"Возвращает статус операции. Поддерживает `?fields=dtFinish,isSuccessful` "
"для поллинга (app-autotest ждёт пока dtFinish != null).\n\n"
"С `?fields=cfsParams` добавляет текущие значения параметров."
),
"operationId": "getOperation",
"parameters": [
{
"name": "instanceOperationUid",
"in": "path",
"required": True,
"schema": {"type": "string", "format": "uuid"},
"description": "UUID операции",
},
{
"name": "fields",
"in": "query",
"schema": {"type": "string"},
"description": "cs-список полей: dtFinish,isSuccessful,cfsParams",
},
],
"responses": {
"200": {
"description": "Статус операции",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/OperationResponse"}}},
},
"404": {
"description": "Операция не найдена",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
},
},
},
# ── Operations: run ──
"/api/v1/svc/instanceOperations/{instanceOperationUid}/run": {
"post": {
"tags": ["operations"],
"summary": "Запустить операцию",
"description": (
"Выполняет операцию синхронно:\n"
"1. Фиксирует dtStart\n"
"2. Ждёт DELAY секунд (настраивается через `/_mock/delay`)\n"
"3. Применяет эффект (create → running, delete → удалить, modify → обновить параметры)\n"
"4. Фиксирует dtFinish\n\n"
"Повторный вызов → 409 (already completed)."
),
"operationId": "runOperation",
"parameters": [
{
"name": "instanceOperationUid",
"in": "path",
"required": True,
"schema": {"type": "string", "format": "uuid"},
"description": "UUID операции",
},
],
"responses": {
"200": {
"description": "Операция выполнена",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/RunResponse"}}},
},
"404": {
"description": "Операция не найдена",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
"409": {
"description": "Операция уже выполнена",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
},
},
},
# ── Operations: validate ──
"/api/v1/svc/instanceOperations/{instanceOperationUid}/validate-cfs": {
"get": {
"tags": ["operations"],
"summary": "Валидация параметров",
"description": "Проверяет параметры операции. Возвращает пустое тело (200).",
"operationId": "validateCfs",
"parameters": [
{
"name": "instanceOperationUid",
"in": "path",
"required": True,
"schema": {"type": "string", "format": "uuid"},
"description": "UUID операции",
},
],
"responses": {
"200": {
"description": "Валидация пройдена",
},
"404": {
"description": "Операция не найдена",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
},
},
},
# ── Operations: set params ──
"/api/v1/svc/instanceOperationCfsParams": {
"post": {
"tags": ["operations"],
"summary": "Установить параметр операции",
"description": (
"Устанавливает значение одного параметра операции. "
"Вызывается N раз (по одному на каждый параметр) перед `/run`."
),
"operationId": "setOperationParam",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/SetParamRequest"},
"example": {
"instanceOperationUid": "550e8400-e29b-41d4-a716-446655440000",
"svcOperationCfsParamId": 1,
"paramValue": "my-value",
},
},
},
},
"responses": {
"200": {
"description": "Параметр установлен (пустое тело)",
},
"400": {
"description": "Не указан instanceOperationUid или svcOperationCfsParamId",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
"404": {
"description": "Операция не найдена",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
},
},
},
# ── Mock ──
"/api/v1/svc/_mock/reset": {
"post": {
"tags": ["mock"],
"summary": "Сброс состояния",
"description": "Удаляет **все** инстансы, операции и параметры. Используется в тестах для изоляции.",
"operationId": "mockReset",
"responses": {
"200": {
"description": "Состояние сброшено",
"content": {"application/json": {"schema": {"type": "object", "properties": {"reset": {"type": "string", "example": "ok"}}}}},
},
},
},
},
"/api/v1/svc/_mock/state": {
"get": {
"tags": ["mock"],
"summary": "Дамп состояния",
"description": "Возвращает все инстансы и операции для отладки.",
"operationId": "mockState",
"responses": {
"200": {
"description": "Текущее состояние",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/MockState"}}},
},
},
},
},
"/api/v1/svc/_mock/services": {
"get": {
"tags": ["mock"],
"summary": "Список загруженных сервисов",
"description": "Возвращает все загруженные сервисы с количеством операций и параметров.",
"operationId": "mockServices",
"responses": {
"200": {
"description": "Список сервисов",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/MockServices"}}},
},
},
},
},
"/api/v1/svc/_mock/delay/{seconds}": {
"post": {
"tags": ["mock"],
"summary": "Задать задержку операций",
"description": (
"Меняет задержку выполнения операций «на лету» без перезапуска.\n"
"Минимум: 0, максимум: 5 (защита /health)."
),
"operationId": "mockSetDelay",
"parameters": [
{
"name": "seconds",
"in": "path",
"required": True,
"schema": {"type": "number", "minimum": 0, "maximum": 5},
"description": "Задержка в секундах (0…5)",
},
],
"responses": {
"200": {
"description": "Задержка изменена",
"content": {"application/json": {"schema": {"type": "object", "properties": {"delay": {"type": "number"}}}}},
},
"400": {
"description": "Некорректное значение",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}},
},
},
},
},
"/api/v1/svc/_mock/fail-next": {
"post": {
"tags": ["mock"],
"summary": "Следующая операция упадёт",
"description": (
"Устанавливает one-shot флаг. Следующий `POST /run` вернёт ошибку.\n"
"Позволяет тестировать негативные сценарии."
),
"operationId": "mockFailNext",
"responses": {
"200": {
"description": "Флаг установлен",
"content": {"application/json": {"schema": {"type": "object", "properties": {"fail_next": {"type": "boolean", "example": True}}}}},
},
},
},
},
}