From ef593101a381f2e6b36f39915c82bd9371e5dc8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 1 Aug 2026 09:41:55 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20Swagger=20UI=20+=20OpenAPI=203.1.0=20?= =?UTF-8?q?=D1=81=D0=BF=D0=B5=D0=BA=D0=B0=20(v0.4.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /swagger — Swagger UI (CDN, без pip-зависимостей) - /api/v1/svc/openapi.json — динамическая OpenAPI 3.1.0 спека - Nubes-брендированный topbar (лого + версия) - Bearer-токен предзаполняется автоматически - 17 эндпоинтов с полными схемами, примерами, описаниями - Ссылка «Swagger» на главной странице --- site/app.py | 2 + site/config/loader.py | 2 +- site/routes/openapi.py | 789 ++++++++++++++++++++++++++++++++++++ site/routes/root.py | 31 +- site/static/style.css | 17 + site/templates/index.html | 1 + site/templates/swagger.html | 142 +++++++ 7 files changed, 977 insertions(+), 7 deletions(-) create mode 100644 site/routes/openapi.py create mode 100644 site/templates/swagger.html diff --git a/site/app.py b/site/app.py index 53921d7..3cc72dd 100644 --- a/site/app.py +++ b/site/app.py @@ -34,6 +34,7 @@ from routes.instances_routes import bp as instances_bp from routes.operations_routes import bp as operations_bp from routes.run import bp as run_bp from routes.mock_routes import bp as mock_bp +from routes.openapi import bp as openapi_bp # Версия — единый источник правды в config/loader.py from config.loader import VERSION @@ -49,6 +50,7 @@ app.register_blueprint(instances_bp) # /api/v1/svc/instances app.register_blueprint(operations_bp) # /api/v1/svc/instanceOperations + /instanceOperationCfsParams app.register_blueprint(run_bp) # /api/v1/svc/instanceOperations//run app.register_blueprint(mock_bp) # /api/v1/svc/_mock/* +app.register_blueprint(openapi_bp) # /api/v1/svc/openapi.json # Локальный запуск (без gunicorn) — только для разработки diff --git a/site/config/loader.py b/site/config/loader.py index fcfae69..2fd327f 100644 --- a/site/config/loader.py +++ b/site/config/loader.py @@ -71,4 +71,4 @@ DELAY = float(os.getenv("MOCK_OP_DELAY", "0.1")) # Версия полигона — единый источник правды для app.py и routes/root.py. # Меняется при КАЖДОМ изменении кода. -VERSION = "0.3.2" +VERSION = "0.4.0" diff --git a/site/routes/openapi.py b/site/routes/openapi.py new file mode 100644 index 0000000..4574395 --- /dev/null +++ b/site/routes/openapi.py @@ -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}}}}}, + }, + }, + }, + }, + } diff --git a/site/routes/root.py b/site/routes/root.py index b495f74..35e13cf 100644 --- a/site/routes/root.py +++ b/site/routes/root.py @@ -1,8 +1,9 @@ """ -routes/root.py — корневые эндпоинты (/health, /). +routes/root.py — корневые эндпоинты (/health, /, /swagger). Blueprint "root" регистрируется в app.py БЕЗ url_prefix. -Отвечает за healthcheck (для Nubes) и HTML-страницу с информацией о сервисе. +Отвечает за healthcheck (для Nubes), HTML-страницу с информацией о сервисе, +и страницу Swagger UI. """ import os @@ -14,6 +15,12 @@ import config.loader as _cfg # Версия — из config/loader.py (единый источник правды) VERSION = _cfg.VERSION +# Токен для авторизации в Swagger UI (предзаполняется) +_MOCK_AUTH_TOKEN = os.getenv("MOCK_AUTH_TOKEN", "test-token-123") + +# URL для OpenAPI-спеке (сервер) +_POLYGON_ENDPOINT = os.getenv("POLYGON_ENDPOINT", "https://polygon.pythonk8s.dev.nubes.ru") + # Blueprint без префикса — роуты /health и / будут на корне домена bp = Blueprint("root", __name__) @@ -52,9 +59,6 @@ def index(): "has_out": bool(svc.get("stateOut")), }) - # URL для примеров — из переменной окружения или автоопределение - endpoint = os.getenv("POLYGON_ENDPOINT", "https://polygon.pythonk8s.dev.nubes.ru") - return render_template( "index.html", version=_cfg.VERSION, @@ -62,5 +66,20 @@ def index(): inst_count=inst_count, delay=_cfg.DELAY, svc_list=svc_list, - endpoint=endpoint, + endpoint=_POLYGON_ENDPOINT, + ) + + +@bp.route("/swagger") +def swagger(): + """Swagger UI — интерактивная документация API. + + Загружает Swagger UI с CDN, указывает на /api/v1/svc/openapi.json. + Токен авторизации предзаполняется автоматически (MOCK_AUTH_TOKEN). + """ + return render_template( + "swagger.html", + version=_cfg.VERSION, + openapi_url="/api/v1/svc/openapi.json", + auth_token=_MOCK_AUTH_TOKEN, ) diff --git a/site/static/style.css b/site/static/style.css index 3f803dc..9f51585 100644 --- a/site/static/style.css +++ b/site/static/style.css @@ -96,6 +96,23 @@ main { color: #fff; } +.swagger-link { + margin-left: auto; + font-size: 13px; + color: var(--text-muted); + text-decoration: none; + padding: 4px 12px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + transition: all 0.15s; +} + +.swagger-link:hover { + color: var(--brand-primary); + border-color: var(--brand-primary); + background: #eff6ff; +} + /* ── Dots ── */ .dot { display: inline-block; diff --git a/site/templates/index.html b/site/templates/index.html index 8d5956c..4111c78 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -14,6 +14,7 @@
polygon v{{ version }} + 📋 Swagger
diff --git a/site/templates/swagger.html b/site/templates/swagger.html new file mode 100644 index 0000000..c8c3428 --- /dev/null +++ b/site/templates/swagger.html @@ -0,0 +1,142 @@ + + + + + + Polygon API — Swagger + + + + + + +
+ + + + + + + +