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:
@@ -34,6 +34,7 @@ from routes.instances_routes import bp as instances_bp
|
|||||||
from routes.operations_routes import bp as operations_bp
|
from routes.operations_routes import bp as operations_bp
|
||||||
from routes.run import bp as run_bp
|
from routes.run import bp as run_bp
|
||||||
from routes.mock_routes import bp as mock_bp
|
from routes.mock_routes import bp as mock_bp
|
||||||
|
from routes.openapi import bp as openapi_bp
|
||||||
|
|
||||||
# Версия — единый источник правды в config/loader.py
|
# Версия — единый источник правды в config/loader.py
|
||||||
from config.loader import VERSION
|
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(operations_bp) # /api/v1/svc/instanceOperations + /instanceOperationCfsParams
|
||||||
app.register_blueprint(run_bp) # /api/v1/svc/instanceOperations/<uid>/run
|
app.register_blueprint(run_bp) # /api/v1/svc/instanceOperations/<uid>/run
|
||||||
app.register_blueprint(mock_bp) # /api/v1/svc/_mock/*
|
app.register_blueprint(mock_bp) # /api/v1/svc/_mock/*
|
||||||
|
app.register_blueprint(openapi_bp) # /api/v1/svc/openapi.json
|
||||||
|
|
||||||
|
|
||||||
# Локальный запуск (без gunicorn) — только для разработки
|
# Локальный запуск (без gunicorn) — только для разработки
|
||||||
|
|||||||
@@ -71,4 +71,4 @@ DELAY = float(os.getenv("MOCK_OP_DELAY", "0.1"))
|
|||||||
|
|
||||||
# Версия полигона — единый источник правды для app.py и routes/root.py.
|
# Версия полигона — единый источник правды для app.py и routes/root.py.
|
||||||
# Меняется при КАЖДОМ изменении кода.
|
# Меняется при КАЖДОМ изменении кода.
|
||||||
VERSION = "0.3.2"
|
VERSION = "0.4.0"
|
||||||
|
|||||||
@@ -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}}}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
+25
-6
@@ -1,8 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
routes/root.py — корневые эндпоинты (/health, /).
|
routes/root.py — корневые эндпоинты (/health, /, /swagger).
|
||||||
|
|
||||||
Blueprint "root" регистрируется в app.py БЕЗ url_prefix.
|
Blueprint "root" регистрируется в app.py БЕЗ url_prefix.
|
||||||
Отвечает за healthcheck (для Nubes) и HTML-страницу с информацией о сервисе.
|
Отвечает за healthcheck (для Nubes), HTML-страницу с информацией о сервисе,
|
||||||
|
и страницу Swagger UI.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -14,6 +15,12 @@ import config.loader as _cfg
|
|||||||
# Версия — из config/loader.py (единый источник правды)
|
# Версия — из config/loader.py (единый источник правды)
|
||||||
VERSION = _cfg.VERSION
|
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 и / будут на корне домена
|
# Blueprint без префикса — роуты /health и / будут на корне домена
|
||||||
bp = Blueprint("root", __name__)
|
bp = Blueprint("root", __name__)
|
||||||
|
|
||||||
@@ -52,9 +59,6 @@ def index():
|
|||||||
"has_out": bool(svc.get("stateOut")),
|
"has_out": bool(svc.get("stateOut")),
|
||||||
})
|
})
|
||||||
|
|
||||||
# URL для примеров — из переменной окружения или автоопределение
|
|
||||||
endpoint = os.getenv("POLYGON_ENDPOINT", "https://polygon.pythonk8s.dev.nubes.ru")
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"index.html",
|
"index.html",
|
||||||
version=_cfg.VERSION,
|
version=_cfg.VERSION,
|
||||||
@@ -62,5 +66,20 @@ def index():
|
|||||||
inst_count=inst_count,
|
inst_count=inst_count,
|
||||||
delay=_cfg.DELAY,
|
delay=_cfg.DELAY,
|
||||||
svc_list=svc_list,
|
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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -96,6 +96,23 @@ main {
|
|||||||
color: #fff;
|
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 ── */
|
/* ── Dots ── */
|
||||||
.dot {
|
.dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<div class="header-top">
|
<div class="header-top">
|
||||||
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Nubes" class="logo" width="150" height="33">
|
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Nubes" class="logo" width="150" height="33">
|
||||||
<span class="badge badge-blue">polygon v{{ version }}</span>
|
<span class="badge badge-blue">polygon v{{ version }}</span>
|
||||||
|
<a href="/swagger" class="swagger-link">📋 Swagger</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-status">
|
<div class="header-status">
|
||||||
<span class="dot dot-green"></span>
|
<span class="dot dot-green"></span>
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Polygon API — Swagger</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
|
||||||
|
<style>
|
||||||
|
html { box-sizing: border-box; overflow-y: scroll; }
|
||||||
|
*, *:before, *:after { box-sizing: inherit; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
/* Nubes-брендированный topbar */
|
||||||
|
.swagger-ui .topbar {
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 4px solid #2563eb;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
.swagger-ui .topbar .wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
max-width: 1460px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
.swagger-ui .topbar a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.swagger-ui .topbar img.logo {
|
||||||
|
height: 28px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.swagger-ui .topbar .version-badge {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 100px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
.swagger-ui .topbar .nav-link {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.swagger-ui .topbar .nav-link:hover {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
/* Скрываем лого Swagger по умолчанию (заменяем своим) */
|
||||||
|
.swagger-ui .topbar .link img[alt="Swagger UI"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
/* Инфо-блок в description */
|
||||||
|
.swagger-ui .markdown p {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.swagger-ui .info .title {
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js" crossorigin></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-standalone-preset.js" crossorigin></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.onload = function () {
|
||||||
|
const ui = SwaggerUIBundle({
|
||||||
|
url: "{{ openapi_url }}",
|
||||||
|
dom_id: "#swagger-ui",
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||||||
|
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
|
||||||
|
layout: "StandaloneLayout",
|
||||||
|
defaultModelsExpandDepth: 1,
|
||||||
|
defaultModelExpandDepth: 2,
|
||||||
|
docExpansion: "list",
|
||||||
|
filter: true,
|
||||||
|
showExtensions: true,
|
||||||
|
showCommonExtensions: true,
|
||||||
|
// Предзаполняем токен
|
||||||
|
onComplete: function () {
|
||||||
|
// Предзаполнить Bearer токен
|
||||||
|
const authToken = "{{ auth_token }}";
|
||||||
|
if (authToken) {
|
||||||
|
// Swagger UI хранит авторизацию в localStorage
|
||||||
|
try {
|
||||||
|
const key = "swagger_auth_bearerAuth_" + encodeURIComponent(window.location.origin);
|
||||||
|
const auth = JSON.parse(localStorage.getItem(key) || "{}");
|
||||||
|
auth.value = "Bearer " + authToken;
|
||||||
|
localStorage.setItem(key, JSON.stringify(auth));
|
||||||
|
ui.preauthorizeApiKey("bearerAuth", authToken);
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestInterceptor: function (req) {
|
||||||
|
// Добавляем заголовок Authorization если Swagger UI его не добавил
|
||||||
|
if (!req.headers.Authorization && "{{ auth_token }}") {
|
||||||
|
req.headers.Authorization = "Bearer {{ auth_token }}";
|
||||||
|
}
|
||||||
|
return req;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Кастомный topbar с лого Nubes
|
||||||
|
const observer = new MutationObserver(function () {
|
||||||
|
const topbar = document.querySelector(".swagger-ui .topbar .wrapper");
|
||||||
|
if (topbar) {
|
||||||
|
observer.disconnect();
|
||||||
|
// Заменяем содержимое ссылки на свой логотип
|
||||||
|
const link = topbar.querySelector("a");
|
||||||
|
if (link) {
|
||||||
|
link.innerHTML = '<img class="logo" src="{{ url_for("static", filename="logo.svg") }}" alt="Nubes">' +
|
||||||
|
'<span class="version-badge">polygon v{{ version }}</span>';
|
||||||
|
link.href = "/";
|
||||||
|
}
|
||||||
|
// Добавляем ссылку «← На главную»
|
||||||
|
const navLink = document.createElement("a");
|
||||||
|
navLink.className = "nav-link";
|
||||||
|
navLink.href = "/";
|
||||||
|
navLink.textContent = "← На главную";
|
||||||
|
topbar.appendChild(navLink);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user