- loader.py: STANDS dict, get_services/get_ops_index per stand, POLYGON_STANDS env
- mock_state.py: get_state(stand_id) — ленивая инициализация
- app.py: StandMiddleware — перезапись PATH_INFO до Flask-роутинга
- Все роуты: LocalProxy(state), LocalProxy(SERVICES/OPS_INDEX) через g.stand_id
- openapi.py: _build_servers() — по одному серверу на стенд
- services/stand{1,2,3}/ — 37 YAML на стенд из STANDS/dev и STANDS/test
- tests/test_api.py: +4 multi-stand теста (изоляция, fail-next, OpenAPI servers)
- Обратная совместимость: /api/v1/svc/... → стенд по умолчанию
106 lines
4.9 KiB
Python
106 lines
4.9 KiB
Python
"""
|
||
polygon v0.5.0 — Mock Nubes API.
|
||
Эмулятор REST API облачной платформы Nubes для интеграционных тестов.
|
||
|
||
Поддержка нескольких стендов:
|
||
URL: /<stand_id>/api/v1/svc/... (stand_id из POLYGON_STANDS)
|
||
Состояние изолировано: у каждого стенда свой набор инстансов и операций.
|
||
|
||
Blueprint'ы (каждый — отдельный файл в routes/):
|
||
- root_bp → /health, / (HTML-страница)
|
||
- services_bp → /api/v1/svc/services, /services/<id>
|
||
- instances_bp → /api/v1/svc/instances (GET list, GET detail, POST create)
|
||
- operations_bp → /api/v1/svc/instanceOperations/* (create, default, status, params, validate)
|
||
- run_bp → /api/v1/svc/instanceOperations/<uid>/run
|
||
- mock_bp → /api/v1/svc/_mock/* (reset, state, services, delay)
|
||
|
||
Деплой: Nubes pythonk8s (managed service) с gunicorn:
|
||
gunicorn app:app --workers 1 --bind 0.0.0.0:8000
|
||
⛔ РОВНО 1 ВОРКЕР — состояние в памяти, не shared между процессами.
|
||
|
||
Переменные окружения:
|
||
POLYGON_STANDS — список стендов: name:path,name:path,...
|
||
MOCK_OP_DELAY — задержка операции в секундах (по умолчанию 0.1)
|
||
WEB_CONCURRENCY — число gunicorn-воркеров (принудительно 1)
|
||
"""
|
||
|
||
import os
|
||
|
||
from flask import Flask, g, request
|
||
|
||
# ⛔ Принудительно 1 воркер — состояние в памяти, иначе инстансы теряются.
|
||
os.environ.setdefault("WEB_CONCURRENCY", "1")
|
||
|
||
# Импорт blueprint'ов — каждый отвечает за свою группу маршрутов
|
||
from routes.root import bp as root_bp
|
||
from routes.services_routes import bp as services_bp
|
||
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
|
||
import config.loader as _cfg
|
||
|
||
# Flask-приложение с Jinja2-шаблонами и статикой
|
||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# WSGI Middleware — перезапись PATH_INFO до Flask-роутинга
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class StandMiddleware:
|
||
"""WSGI middleware: извлекает stand_id из URL и перезаписывает PATH_INFO.
|
||
|
||
/stand1/api/v1/svc/... → environ['stand_id']='stand1', PATH_INFO=/api/v1/svc/...
|
||
/api/v1/svc/... → environ['stand_id']=DEFAULT_STAND, без изменений
|
||
/health, /swagger, / → environ['stand_id']=DEFAULT_STAND, без изменений
|
||
|
||
Должен быть ДО Flask-роутинга, поэтому это middleware, а не before_request.
|
||
"""
|
||
|
||
def __init__(self, wsgi_app):
|
||
self.wsgi_app = wsgi_app
|
||
|
||
def __call__(self, environ, start_response):
|
||
path = environ.get("PATH_INFO", "/")
|
||
parts = path.split("/")
|
||
|
||
if len(parts) > 1 and parts[1] in _cfg.STANDS:
|
||
environ["polygon.stand_id"] = parts[1]
|
||
new_path = "/" + "/".join(parts[2:]) if len(parts) > 2 else "/"
|
||
environ["PATH_INFO"] = new_path
|
||
else:
|
||
environ["polygon.stand_id"] = _cfg.DEFAULT_STAND
|
||
|
||
return self.wsgi_app(environ, start_response)
|
||
|
||
|
||
app.wsgi_app = StandMiddleware(app.wsgi_app)
|
||
|
||
|
||
@app.before_request
|
||
def _set_stand():
|
||
"""Копирует stand_id из environ в g."""
|
||
g.stand_id = request.environ.get("polygon.stand_id", _cfg.DEFAULT_STAND)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
# Регистрируем blueprint'ы.
|
||
app.register_blueprint(root_bp) # /health, /
|
||
app.register_blueprint(services_bp) # /api/v1/svc/services
|
||
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/<uid>/run
|
||
app.register_blueprint(mock_bp) # /api/v1/svc/_mock/*
|
||
app.register_blueprint(openapi_bp) # /api/v1/svc/openapi.json
|
||
|
||
|
||
# Локальный запуск (без gunicorn) — только для разработки
|
||
if __name__ == "__main__":
|
||
app.run(debug=True, host="0.0.0.0", port=5000)
|