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» на главной странице
This commit is contained in:
2026-08-01 09:41:55 +04:00
parent 691e9f4cbd
commit ef593101a3
7 changed files with 977 additions and 7 deletions
+789
View File
@@ -0,0 +1,789 @@
"""
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}}}}},
},
},
},
},
}