Compare commits
10
Commits
4ee0433e28
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b78bae571e | ||
|
|
366a6754fe | ||
|
|
9ca9f8ba54 | ||
|
|
f67facae20 | ||
|
|
cd06040d62 | ||
|
|
af2cede717 | ||
|
|
c64812b039 | ||
|
|
18d08ccccd | ||
|
|
cfc341db44 | ||
|
|
333a3d5e63 |
@@ -0,0 +1,132 @@
|
|||||||
|
# Code Review: app-autotest v1.2.39 — Соннет
|
||||||
|
|
||||||
|
Дата: 2026-08-03
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Критические
|
||||||
|
|
||||||
|
**1. XSS в instances.js — `o.operation` в innerHTML без `_esc()`**
|
||||||
|
|
||||||
|
instances.js (функция `toggleInstance`):
|
||||||
|
```js
|
||||||
|
opsEl.innerHTML = ops.map(o =>
|
||||||
|
`<button ... onclick="runOp('${o.operation}',${o.svcOperationId})">${o.operation}</button>`
|
||||||
|
).join('');
|
||||||
|
```
|
||||||
|
`o.operation` вставляется **три раза без `_esc()`**: в onclick-атрибут (`'${...}'`), в текст кнопки (`>${...}<`). Если API вернёт `operation = "'; alert(1)//"` — onclick-атрибут ломается. На практике операции — это "modify"/"delete" из Nubes API, но принцип нарушен. `_esc()` определён специально для этого.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 Важные
|
||||||
|
|
||||||
|
**2. Двойной вызов `detect_endpoint()` при каждом HTTP-запросе**
|
||||||
|
|
||||||
|
auth.py — `get_client()` вызывает `detect_endpoint(token)`, и `get_stand()` вызывает `detect_endpoint(token)` независимо. В api_test.py при `POST /api/test`:
|
||||||
|
```python
|
||||||
|
client = get_client() # detect_endpoint() #1
|
||||||
|
...
|
||||||
|
get_client_id(), get_stand() # detect_endpoint() #2
|
||||||
|
```
|
||||||
|
Итого **2 лишних HTTP-запроса к Nubes API** (dev + test) при каждом запросе в облачном режиме. Оба блока `if endpoint in STANDS` срабатывают при стандартной конфигурации.
|
||||||
|
|
||||||
|
**3. `mode` и `polygon_stand` не валидируются при `set_mode`**
|
||||||
|
|
||||||
|
main.py:
|
||||||
|
```python
|
||||||
|
new_mode = request.form.get("mode", "polygon")
|
||||||
|
new_stand = request.form.get("polygon_stand") or request.cookies.get("polygon_stand", "test")
|
||||||
|
```
|
||||||
|
Любое значение пишется в cookie. Если `polygon_stand = "evil/../../../etc"`, то в `get_client()` формируется URL:
|
||||||
|
```
|
||||||
|
polygon_url.replace("/api/v1/svc", "/evil/../../../etc/api/v1/svc")
|
||||||
|
```
|
||||||
|
Путь нормализуется HTTP-клиентом/сервером. Допустимые значения известны: `mode` ∈ {"polygon","cloud"}, `polygon_stand` ∈ {"dev","test","prod"} — надо добавить whitelist.
|
||||||
|
|
||||||
|
**4. В polygon-режиме у всех пользователей одинаковый `client_id`**
|
||||||
|
|
||||||
|
auth.py:
|
||||||
|
```python
|
||||||
|
def get_token():
|
||||||
|
if get_mode() == "polygon":
|
||||||
|
return current_app.config["NUBES_API_TOKEN"] # env-токен
|
||||||
|
```
|
||||||
|
`get_client_id()` тоже парсит env-токен → у всех пользователей в эмуляции одинаковый ClientID. Вся изоляция данных в БД (`runs`, `scenario_runs`, `scenario_definitions`) — по `(client_id, stand)`. Если два пользователя переключатся в polygon-режим — они видят историю и сценарии **друг друга**. Если приложение использует только один человек — не проблема. Если несколько — критично.
|
||||||
|
|
||||||
|
**5. `api_history` — соединение не в `finally`**
|
||||||
|
|
||||||
|
api_test.py:
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(...)
|
||||||
|
...
|
||||||
|
cur.close()
|
||||||
|
put_conn(conn) # явный return соединения
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
try: cur.close() # NameError если исключение до cur = ...
|
||||||
|
except: pass
|
||||||
|
try: put_conn(conn) # NameError если исключение до conn = ...
|
||||||
|
except: pass
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
```
|
||||||
|
Все остальные функции (`api_scenario_run_status`, `api_scenario_status`) используют `finally: put_conn(conn)` — корректный паттерн. Здесь — нет. При ошибке до `cur.close()` соединение возвращается, но `cur` не закрывается. Не утечка (psycopg2 закроет при возврате conn в пул), но несоответствие стилю.
|
||||||
|
|
||||||
|
**6. `stand_name()` — подстрочный поиск**
|
||||||
|
|
||||||
|
http_client.py:
|
||||||
|
```python
|
||||||
|
for name in ("dev", "test"):
|
||||||
|
if name in (endpoint or ""):
|
||||||
|
return name
|
||||||
|
```
|
||||||
|
Если URL содержит "dev" как часть другого слова (например, "development", "devnull"), вернёт неверное значение. URL-то сейчас конкретные, но хрупко.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 Рекомендации
|
||||||
|
|
||||||
|
**7. Двойной отступ в scenario.py**
|
||||||
|
|
||||||
|
scenario.py:
|
||||||
|
```python
|
||||||
|
for i, step in enumerate(steps):
|
||||||
|
step_num = i + 1 # 8 пробелов вместо 4
|
||||||
|
```
|
||||||
|
Весь цикл имеет нестандартный отступ (8 пробелов). Работает корректно (Python), но выглядит как след удалённого `try:` или `with:` блока, который был внутри for.
|
||||||
|
|
||||||
|
**8. `get_real_client()` — мёртвый алиас**
|
||||||
|
|
||||||
|
auth.py: функция идентична `get_client()`, есть только для обратной совместимости. В main.py и других файлах есть вызовы `get_real_client()`. Стоит постепенно заменить на `get_client()` и убрать алиас.
|
||||||
|
|
||||||
|
**9. Нет `UNIQUE` на `runs.op_uid`**
|
||||||
|
|
||||||
|
init_db.py: ручной UPSERT в save_run.py (UPDATE → INSERT) предполагает уникальность `op_uid`. Индекса нет. При маловероятной гонке двух параллельных `save_run` с одним `op_uid` — оба сделают UPDATE (rowcount=0) → оба сделают INSERT → дубль. Добавить `CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_op_uid ON runs (op_uid) WHERE op_uid IS NOT NULL`.
|
||||||
|
|
||||||
|
**10. Ротация лога: `flock(LOCK_UN)` перед flush буфера**
|
||||||
|
|
||||||
|
api_test.py:
|
||||||
|
```python
|
||||||
|
f.write(rest)
|
||||||
|
fcntl.flock(f, fcntl.LOCK_UN) # лок снят, но буфер ещё не сброшен
|
||||||
|
```
|
||||||
|
Python-буфер сбрасывается при закрытии файла (`with`-блок), но лок уже снят. Другой воркер может прочитать неполные данные. Добавить `f.flush()` перед `LOCK_UN`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Итого по приоритетам
|
||||||
|
|
||||||
|
| # | Файл | Проблема | Серьёзность |
|
||||||
|
|---|------|----------|-------------|
|
||||||
|
| 1 | `static/js/instances.js` | `o.operation` в innerHTML без `_esc()` | 🔴 |
|
||||||
|
| 2 | `api/auth.py` | Двойной `detect_endpoint()` на запрос | 🟡 |
|
||||||
|
| 3 | `routes/main.py` | `mode`/`polygon_stand` без whitelist-валидации | 🟡 |
|
||||||
|
| 4 | `api/auth.py` | Shared `client_id` в polygon-режиме | 🟡 |
|
||||||
|
| 5 | `routes/api_test.py` | `api_history` без `finally` | 🟡 |
|
||||||
|
| 6 | `api/http_client.py` | `stand_name()` substring match | 🟡 |
|
||||||
|
| 7 | `operations/scenario.py` | Двойной отступ в for-цикле | 🟢 |
|
||||||
|
| 8 | `api/auth.py` | `get_real_client()` мёртвый алиас | 🟢 |
|
||||||
|
| 9 | `db/init_db.py` | Нет UNIQUE индекса на `runs.op_uid` | 🟢 |
|
||||||
|
| 10 | `routes/api_test.py` | `flock(LOCK_UN)` перед flush в ротации | 🟢 |
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# 2026-08-03-session — Полигон: интеграция, баг с сервисами
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
Сессия началась с чтения двух файлов AI-агентом:
|
||||||
|
- `AGENT_BRIEFING.md` — контекст app-autotest (v1.2.28)
|
||||||
|
- `polygon-docs/POLYGON-FULL.md` — полное описание полигона (v0.5.9)
|
||||||
|
|
||||||
|
jsonEnv автотеста на проде:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"POLYGON_ENDPOINT": "https://polygon.pythonk8s.dev.nubes.ru/api/v1/svc",
|
||||||
|
"NUBES_API_ENDPOINT": "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Архитектурное решение: разделение сервисы/инстансы
|
||||||
|
|
||||||
|
**Зафиксировано (2026-08-03):**
|
||||||
|
|
||||||
|
| Режим | Сервисы (метаданные) | Инстансы |
|
||||||
|
|--------|---------------------|-----------|
|
||||||
|
| Без полигона | Реальный API | Реальное облако |
|
||||||
|
| С полигоном | Реальный API | Виртуальные (полигон) |
|
||||||
|
|
||||||
|
`POLYGON_ENDPOINT` подменяет ТОЛЬКО инстансы/операции. Сервисы — это «справочник», всегда из реального API.
|
||||||
|
|
||||||
|
### Где это в коде
|
||||||
|
|
||||||
|
`auth.py:_make_client(use_polygon)`:
|
||||||
|
- `use_polygon=True` → `get_client()`: если `POLYGON_ENDPOINT` задан → полигон, иначе → `NUBES_API_ENDPOINT`
|
||||||
|
- `use_polygon=False` → `get_real_client()`: **всегда** `NUBES_API_ENDPOINT`
|
||||||
|
|
||||||
|
## Баг: «нет сервисов» при POLYGON_ENDPOINT
|
||||||
|
|
||||||
|
### Причина
|
||||||
|
|
||||||
|
Цепочка:
|
||||||
|
1. `get_stand()` → `"polygon"` (auth.py:141, POLYGON_ENDPOINT задан)
|
||||||
|
2. `load_service_ids("polygon")` → `services_polygon.txt` не существует → пустой `set`
|
||||||
|
3. `sorted(пустой_set)` → `[]`
|
||||||
|
4. Шаблон `index.html:81`: `{% if svc.svcId in config.service_ids %}` → `svc.svcId in []` → **всегда False** → ни один сервис не показывается
|
||||||
|
|
||||||
|
Хотя `get_real_client()` корректно получает сервисы из реального API, фильтр в шаблоне убивает весь список.
|
||||||
|
|
||||||
|
### Исправление (v1.2.29)
|
||||||
|
|
||||||
|
Шаблон `index.html:81`: добавить `or not config.service_ids`:
|
||||||
|
|
||||||
|
```jinja2
|
||||||
|
{# Было #}
|
||||||
|
{% if svc.svcId in config.service_ids %}
|
||||||
|
|
||||||
|
{# Стало #}
|
||||||
|
{% if not config.service_ids or svc.svcId in config.service_ids %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Это соответствует изначальному замыслу `load_service_ids`: «пустой set → показываем все».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 2: селектор режима и стенда (v1.2.30)
|
||||||
|
|
||||||
|
### Требования
|
||||||
|
|
||||||
|
1. **Селектор режима** над версией: 🎭 Эмуляция / ☁️ Облако (только если POLYGON_ENDPOINT задан)
|
||||||
|
2. **Селектор стенда** (DEV/TEST/PROD) — только в режиме эмуляции
|
||||||
|
3. **По умолчанию:** эмуляция + TEST
|
||||||
|
4. **В эмуляции:** токен дезактивирован
|
||||||
|
5. **В облаке:** стенд автоопределяется по токену (как раньше)
|
||||||
|
6. При смене режима/стенда → сервисы обновляются
|
||||||
|
|
||||||
|
### Новая архитектура
|
||||||
|
|
||||||
|
| Режим | Сервисы | Инстансы | Стенд |
|
||||||
|
|--------|---------|----------|-------|
|
||||||
|
| Эмуляция | Полигон (/{stand}/api/v1/svc) | Полигон | Выбор (dev/test/prod) |
|
||||||
|
| Облако | Реальный API | Реальный API | Авто (токен→dev/test) |
|
||||||
|
|
||||||
|
В эмуляции **всё** идёт в полигон — и сервисы, и инстансы. Полигон имеет полный набор эндпоинтов (/services, /instances, /instanceOperations).
|
||||||
|
|
||||||
|
`get_real_client()` упраздняется — становится алиасом `get_client()`.
|
||||||
|
|
||||||
|
### Cookie
|
||||||
|
|
||||||
|
- `mode` — `"polygon"` (по умолчанию) или `"cloud"`
|
||||||
|
- `polygon_stand` — `"test"` (по умолчанию), `"dev"`, `"prod"`
|
||||||
|
|
||||||
|
### Изменённые файлы
|
||||||
|
|
||||||
|
1. **`auth.py`** — новые функции `get_mode()`, `get_polygon_stand()`; `get_token()` в режиме полигона только env-токен; `get_client()` строит URL с префиксом стенда; `get_real_client()` → алиас
|
||||||
|
2. **`main.py`** — `action=set_mode` (cookie), single `get_client()` вместо real_client/inst_client, переменные `mode`/`polygon_stand`/`polygon_enabled`
|
||||||
|
3. **`index.html`** — селекторы над версией, disable токена в эмуляции
|
||||||
|
4. **`service_list.py`** — обрезать `polygon_` префикс
|
||||||
|
5. **`api_test.py`** — `get_real_client()` → `get_client()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 3: Код-ревью Соннета 4.6 (v1.2.39)
|
||||||
|
|
||||||
|
### Промпт
|
||||||
|
|
||||||
|
```
|
||||||
|
Ты — senior backend-разработчик, делаешь код-ревью веб-приложения app-autotest.
|
||||||
|
Flask 3.1 + vanilla JS + PostgreSQL. Два режима: эмуляция (Polygon) / облако.
|
||||||
|
Проверь внимательно: безопасность, URL, гонки, целостность данных, ошибки, архитектуру, JS.
|
||||||
|
Для каждой находки: 🔴 критическое / 🟡 важное / 🟢 рекомендация.
|
||||||
|
Если неясно — спроси, устроим диалог.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Результаты
|
||||||
|
|
||||||
|
| # | Файл | Проблема | Серьёзность |
|
||||||
|
|---|------|----------|-------------|
|
||||||
|
| 1 | `static/js/instances.js` | `o.operation` в innerHTML без `_esc()` | 🔴 |
|
||||||
|
| 2 | `api/auth.py` | Двойной `detect_endpoint()` на запрос | 🟡 |
|
||||||
|
| 3 | `routes/main.py` | `mode`/`polygon_stand` без whitelist-валидации | 🟡 |
|
||||||
|
| 4 | `api/auth.py` | Общий `client_id` в polygon-режиме (env-токен) | 🟡 |
|
||||||
|
| 5 | `routes/api_test.py` | `api_history` без `finally` для conn | 🟡 |
|
||||||
|
| 6 | `api/http_client.py` | `stand_name()` substring match | 🟡 |
|
||||||
|
| 7 | `operations/scenario.py` | Двойной отступ в for-цикле | 🟢 |
|
||||||
|
| 8 | `api/auth.py` | `get_real_client()` мёртвый алиас | 🟢 |
|
||||||
|
| 9 | `db/init_db.py` | Нет UNIQUE индекса на `runs.op_uid` | 🟢 |
|
||||||
|
| 10 | `routes/api_test.py` | `flock(LOCK_UN)` перед flush в ротации лога | 🟢 |
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
# Polygon — эмулятор Nubes API
|
||||||
|
|
||||||
|
> Версия: v0.5.9 | Деплой: `polygon.pythonk8s.dev.nubes.ru` | Формат: Nubes managed Flask
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что такое Polygon
|
||||||
|
|
||||||
|
Polygon — **эмулятор REST API облачной платформы Nubes** для интеграционных тестов.
|
||||||
|
|
||||||
|
Он полностью повторяет контракты реального API (сервисы, инстансы, операции, параметры, валидацию),
|
||||||
|
но работает **без реальной инфраструктуры** — в памяти, с мгновенным откликом.
|
||||||
|
|
||||||
|
### Зачем нужен
|
||||||
|
|
||||||
|
- Тестировать создание/изменение/удаление инстансов без реальных облачных ресурсов
|
||||||
|
- Отлаживать UI автодеплоя (`app-autotest`) на мок-данных
|
||||||
|
- Писать интеграционные тесты с детерминированным состоянием
|
||||||
|
- Проверять краевые случаи (ошибки валидации, сбои операций)
|
||||||
|
- Демонстрировать API заказчикам через Swagger UI
|
||||||
|
|
||||||
|
### Что НЕ делает
|
||||||
|
|
||||||
|
- Не управляет реальной инфраструктурой
|
||||||
|
- Не хранит данные между перезапусками (всё в памяти)
|
||||||
|
- Не авторизует пользователей (кроме `_mock/*` служебных эндпоинтов)
|
||||||
|
- Не повторяет ВСЕ эндпоинты реального API — только те что нужны для тестов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Архитектура
|
||||||
|
|
||||||
|
### Технологический стек
|
||||||
|
|
||||||
|
| Компонент | Технология | Зачем |
|
||||||
|
|-----------|-----------|-------|
|
||||||
|
| Веб-фреймворк | Flask 3.0 | Маршрутизация, шаблоны, JSON-ответы |
|
||||||
|
| WSGI-сервер | gunicorn | Запуск на проде (встроен в Nubes managed Flask) |
|
||||||
|
| Конфигурация | YAML (PyYAML) | Описание сервисов, операций, параметров |
|
||||||
|
| Состояние | In-memory dict | Инстансы, операции, параметры |
|
||||||
|
| Шаблоны | Jinja2 | HTML-страницы (главная, Swagger) |
|
||||||
|
| OpenAPI | OpenAPI 3.1.0 | Спека API (генерится динамически) |
|
||||||
|
| Swagger UI | Swagger UI 5 (CDN) | Интерактивная документация |
|
||||||
|
|
||||||
|
### Ключевое ограничение: 1 воркер
|
||||||
|
|
||||||
|
```python
|
||||||
|
# app.py
|
||||||
|
os.environ.setdefault("WEB_CONCURRENCY", "1")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Почему:** состояние (инстансы, операции) хранится в памяти Python-процесса.
|
||||||
|
Два воркера = два набора инстансов = хаос. Поэтому жёстко 1 gunicorn-воркер.
|
||||||
|
|
||||||
|
**Следствия:**
|
||||||
|
- Только 1 запрос обрабатывается одновременно
|
||||||
|
- Большие ответы (>20KB) могут обрываться из-за таймаута сети
|
||||||
|
- Swagger-спека встроена прямо в HTML чтобы избежать второго запроса
|
||||||
|
- Нельзя горизонтально масштабировать
|
||||||
|
|
||||||
|
### Модель данных в памяти
|
||||||
|
|
||||||
|
```
|
||||||
|
MockState (синглтон на стенд)
|
||||||
|
├── instances: {instanceUid → {instanceUid, serviceId, displayName, status, state, ...}}
|
||||||
|
├── operations: {opUid → {instanceOperationUid, instanceUid, operation, dtStart, dtFinish, ...}}
|
||||||
|
├── op_params: {opUid → {paramId(int) → paramValue(str)}}
|
||||||
|
└── fail_next: bool (one-shot флаг для симуляции ошибок)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Жизненный цикл запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP-запрос → Nubes ingress → gunicorn (1 воркер) → Flask
|
||||||
|
→ StandMiddleware (извлекает stand_id из URL, перезаписывает PATH_INFO)
|
||||||
|
→ before_request (копирует stand_id в flask.g)
|
||||||
|
→ Blueprint-роут (через LocalProxy резолвит state/SERVICES под текущий стенд)
|
||||||
|
→ JSON-ответ
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Стенды (dev / test / prod)
|
||||||
|
|
||||||
|
Polygon эмулирует **три изолированных стенда** облака:
|
||||||
|
|
||||||
|
| Стенд | URL-префикс | Сервисов | YAML из |
|
||||||
|
|-------|------------|----------|---------|
|
||||||
|
| dev | `/dev/api/v1/svc/...` | 37 | `~/tf_provider/generated/dev/` |
|
||||||
|
| test | `/test/api/v1/svc/...` | 37 | `~/tf_provider/generated/test/` |
|
||||||
|
| prod | `/prod/api/v1/svc/...` | 35 | `~/tf_provider/generated/prod/` |
|
||||||
|
|
||||||
|
**Изоляция:** у каждого стенда **свои** инстансы, операции, параметры.
|
||||||
|
`fail-next` на dev не влияет на test. Состояние каждого стенда независимо.
|
||||||
|
|
||||||
|
**Переключение в Swagger:** выпадающий список серверов (Server selector).
|
||||||
|
|
||||||
|
**Переключение в URL:** просто добавь префикс — `/dev/`, `/test/`, `/prod/`.
|
||||||
|
Без префикса — стенд по умолчанию (dev).
|
||||||
|
|
||||||
|
**Настройка:** переменная окружения `POLYGON_STANDS`:
|
||||||
|
```
|
||||||
|
POLYGON_STANDS=dev:services/dev,test:services/test,prod:services/prod
|
||||||
|
```
|
||||||
|
Формат: `имя_стенда:путь_к_YAML,имя_стенда:путь_к_YAML,...`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. YAML-пайплайн (откуда берутся сервисы)
|
||||||
|
|
||||||
|
Polygon не знает о сервисах сам — он читает их из YAML-конфигов,
|
||||||
|
которые генерируются из терраформ-репы.
|
||||||
|
|
||||||
|
### Цепочка
|
||||||
|
|
||||||
|
```
|
||||||
|
~/tf_provider/generated/{dev,test,prod}/resources_yaml/*.yaml ← источник (терраформ)
|
||||||
|
│
|
||||||
|
│ from_stands.py — конвертация формата (ВРУЧНУЮ)
|
||||||
|
│ • дедупликация параметров (один param в create/modify/delete → одна запись)
|
||||||
|
│ • построение cfsParamsByOp (какие параметры к какой операции)
|
||||||
|
│ • сбор stateParams из create-операции
|
||||||
|
│ • генерация stateOut из subresource-операций
|
||||||
|
│ • раскодирование HTML-entities (" → ")
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
polygon/site/services/{dev,test,prod}/*.yaml ← скоммичены в git
|
||||||
|
│
|
||||||
|
│ config/loader.py — загрузка ВСЕХ YAML в память при старте
|
||||||
|
│ • _load_one() для каждой папки стенда
|
||||||
|
│ • STANDS = {dev: {SERVICES, OPS_INDEX}, test: {...}, prod: {...}}
|
||||||
|
│ • ~0.7 MB на 3 стенда
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Память Flask-процесса → API отдаёт данные из памяти (без диска)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Когда обновлять YAML
|
||||||
|
|
||||||
|
Когда терраформ-репа обновилась (добавили сервис, изменили параметры):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd polygon/site
|
||||||
|
python from_stands.py ~/tf_provider/generated/dev/resources_yaml services/dev
|
||||||
|
python from_stands.py ~/tf_provider/generated/test/resources_yaml services/test
|
||||||
|
python from_stands.py ~/tf_provider/generated/prod/resources_yaml services/prod
|
||||||
|
cd .. && git add services/ && git commit -m "regenerate YAML" && git push
|
||||||
|
# → редеплоить polygon через Nubes UI
|
||||||
|
```
|
||||||
|
|
||||||
|
**Это ручная операция.** Нет автоматического триггера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Структура кода
|
||||||
|
|
||||||
|
```
|
||||||
|
polygon/
|
||||||
|
├── requirements.txt # Flask>=3.0, gunicorn>=21.2, PyYAML>=6.0
|
||||||
|
├── Procfile # web: gunicorn site.app:app --bind 0.0.0.0:8000
|
||||||
|
├── tests/
|
||||||
|
│ ├── test_api.py # 39 smoke-тестов против деплоя
|
||||||
|
│ ├── fuzz_test.py # 147 фаззинг-тестов (злонамеренные входные данные)
|
||||||
|
│ ├── compare_test.py # Сравнение полигона с реальным Nubes API
|
||||||
|
│ ├── test_converter.py # Юнит-тесты from_stands.py
|
||||||
|
│ └── test_state_machine.py # Юнит-тесты apply_effect()
|
||||||
|
└── site/
|
||||||
|
├── app.py # Flask-приложение: StandMiddleware, blueprint'ы
|
||||||
|
├── mock_state.py # MockState — хранилище инстансов/операций
|
||||||
|
├── state_machine.py # apply_effect() — мутация состояния инстанса
|
||||||
|
├── from_stands.py # Конвертер terraform YAML → polygon YAML
|
||||||
|
├── config/
|
||||||
|
│ └── loader.py # Загрузка YAML → SERVICES, OPS_INDEX, STANDS
|
||||||
|
├── routes/
|
||||||
|
│ ├── root.py # /health, /, /swagger
|
||||||
|
│ ├── services_routes.py # /api/v1/svc/services
|
||||||
|
│ ├── instances_routes.py # /api/v1/svc/instances
|
||||||
|
│ ├── operations_routes.py # /api/v1/svc/instanceOperations/*
|
||||||
|
│ ├── run.py # /api/v1/svc/instanceOperations/<uid>/run
|
||||||
|
│ ├── mock_routes.py # /api/v1/svc/_mock/*
|
||||||
|
│ └── openapi.py # /api/v1/svc/openapi.json (OpenAPI 3.1.0 спека)
|
||||||
|
├── services/
|
||||||
|
│ ├── dev/ # 37 YAML-конфигов (стенд dev)
|
||||||
|
│ ├── test/ # 37 YAML-конфигов (стенд test)
|
||||||
|
│ └── prod/ # 35 YAML-конфигов (стенд prod)
|
||||||
|
├── static/
|
||||||
|
│ ├── style.css # Дизайн-система Nubes
|
||||||
|
│ ├── logo.svg # Логотип
|
||||||
|
│ └── favicon.svg # Иконка
|
||||||
|
├── templates/
|
||||||
|
│ ├── index.html # Главная страница (инфо + ссылки на стенды)
|
||||||
|
│ └── swagger.html # Swagger UI (спека встроена в HTML)
|
||||||
|
└── utils/
|
||||||
|
├── now.py # now() — UTC ISO с 'Z'
|
||||||
|
└── pluralize.py # pluralize() — плюрализация
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ключевые архитектурные решения
|
||||||
|
|
||||||
|
**LocalProxy** — все роуты используют `werkzeug.local.LocalProxy` для доступа к `state` и `SERVICES`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# В каждом роуте:
|
||||||
|
from flask import g
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
import mock_state, config.loader as _cfg
|
||||||
|
|
||||||
|
state = LocalProxy(lambda: mock_state.get_state(g.stand_id))
|
||||||
|
SERVICES = LocalProxy(lambda: _cfg.get_services(g.stand_id))
|
||||||
|
```
|
||||||
|
|
||||||
|
Это позволяет одному и тому же коду роута работать с разными стендами — `g.stand_id` определяет какой стенд используется.
|
||||||
|
|
||||||
|
**StandMiddleware** (app.py) — WSGI-middleware который извлекает `stand_id` из URL ДО Flask-роутинга:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class StandMiddleware:
|
||||||
|
def __call__(self, environ, start_response):
|
||||||
|
path = environ["PATH_INFO"]
|
||||||
|
parts = path.split("/")
|
||||||
|
if parts[1] in STANDS:
|
||||||
|
environ["polygon.stand_id"] = parts[1]
|
||||||
|
environ["PATH_INFO"] = "/" + "/".join(parts[2:]) # /dev/api/... → /api/...
|
||||||
|
else:
|
||||||
|
environ["polygon.stand_id"] = DEFAULT_STAND
|
||||||
|
return self.wsgi_app(environ, start_response)
|
||||||
|
```
|
||||||
|
|
||||||
|
**before_request** (app.py) — копирует stand_id в `flask.g`:
|
||||||
|
```python
|
||||||
|
@app.before_request
|
||||||
|
def _set_stand():
|
||||||
|
g.stand_id = request.environ.get("polygon.stand_id", DEFAULT_STAND)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. API — 17 эндпоинтов
|
||||||
|
|
||||||
|
| Метод | Путь | Назначение | Ответ |
|
||||||
|
|-------|------|------------|-------|
|
||||||
|
| GET | `/health` | Healthcheck для Nubes | `OK` (text) |
|
||||||
|
| GET | `/` | HTML с инфо + ссылки на стенды | HTML |
|
||||||
|
| GET | `/swagger` | Swagger UI (спека в HTML) | HTML |
|
||||||
|
| GET | `/api/v1/svc/openapi.json` | OpenAPI 3.1.0 спека | JSON |
|
||||||
|
| GET | `/api/v1/svc/services` | Список сервисов | `{results: [...]}` |
|
||||||
|
| GET | `/api/v1/svc/services/{id}` | Детали сервиса (операции) | `{svc: {operations: [...]}}` |
|
||||||
|
| GET | `/api/v1/svc/instances` | Список инстансов (пагинация) | `{results, pageSize, page, total}` |
|
||||||
|
| GET | `/api/v1/svc/instances/{uid}` | Полные данные инстанса | `{instance: {...}}` |
|
||||||
|
| POST | `/api/v1/svc/instances` | Создать инстанс | `201 + Location + {instanceUid}` |
|
||||||
|
| GET | `/api/v1/svc/instanceOperations/default/{id}` | Шаблон операции (cfsParams) | `{svcOperation: {cfsParams: [...]}}` |
|
||||||
|
| POST | `/api/v1/svc/instanceOperations` | Создать операцию | `201 + Location + {instanceOperationUid}` |
|
||||||
|
| GET | `/api/v1/svc/instanceOperations/{uid}` | Статус операции | `{instanceOperation: {...}}` |
|
||||||
|
| POST | `/api/v1/svc/instanceOperationCfsParams` | Установить параметр | `{}` |
|
||||||
|
| GET | `/api/v1/svc/instanceOperations/{uid}/validate-cfs` | Валидация параметров | `""` (пустое тело) |
|
||||||
|
| POST | `/api/v1/svc/instanceOperations/{uid}/run` | Выполнить операцию | `{ok: true/false, error?}` |
|
||||||
|
| POST | `/api/v1/svc/_mock/reset` | Сброс состояния | `{reset: "ok"}` |
|
||||||
|
| GET | `/api/v1/svc/_mock/state` | Дамп состояния (отладка) | `{instances, operations}` |
|
||||||
|
| GET | `/api/v1/svc/_mock/services` | Список загруженных сервисов (отладка) | `{count, services}` |
|
||||||
|
| POST | `/api/v1/svc/_mock/delay/{s}` | Задать задержку операций (max 5s) | `{delay: N}` |
|
||||||
|
| POST | `/api/v1/svc/_mock/fail-next` | Следующая операция упадёт (one-shot) | `{fail_next: true}` |
|
||||||
|
|
||||||
|
### Типовой сценарий: создать инстанс и запустить операцию
|
||||||
|
|
||||||
|
Это основной флоу, который повторяет логику реального облачного API.
|
||||||
|
Все примеры — с jq для наглядности, но работают и с `python3 -m json.tool`.
|
||||||
|
|
||||||
|
**Шаг 1. Узнать ID сервиса по имени:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "$URL/services" | jq '.results[] | select(.svc == "НазваниеСервиса") | .svcId'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 2. Узнать ID операции у этого сервиса:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "$URL/services/$SVC_ID" | jq '.svc.operations[] | select(.operation == "create") | .svcOperationId'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 3. Получить список входных параметров (cfsParams):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "$URL/instanceOperations/default/$OP_ID" | jq '.svcOperation.cfsParams[] | {svcOperationCfsParamId, svcOperationCfsParam, dataType, valueList, isRequired}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Ответ показывает для каждого параметра: числовой ID, код, тип данных, список допустимых значений (если есть), обязательность.
|
||||||
|
|
||||||
|
**Шаг 4. Создать инстанс:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "$URL/instances" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"serviceId": '$SVC_ID', "displayName": "мой тестовый инстанс"}' \
|
||||||
|
| jq '.instanceUid'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 5. Создать операцию:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "$URL/instanceOperations" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"instanceUid": "'$INST_UID'", "operation": "create", "svcOperationId": '$OP_ID'}' \
|
||||||
|
| jq '.instanceOperationUid'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 6. Установить параметры (повторить для каждого):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "$URL/instanceOperationCfsParams" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"instanceOperationUid": "'$OP_UID'", "svcOperationCfsParamId": '$PARAM_ID', "paramValue": "значение"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 7. Проверить валидацию и запустить:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверить что все параметры валидны (200 = OK)
|
||||||
|
curl -s -o /dev/null -w "%{http_code}" "$URL/instanceOperations/$OP_UID/validate-cfs"
|
||||||
|
|
||||||
|
# Запустить операцию (будет ждать DELAY секунд)
|
||||||
|
curl -s -X POST "$URL/instanceOperations/$OP_UID/run" | jq '{ok, error}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 8. Проверить результат:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "$URL/instanceOperations/$OP_UID?fields=dtFinish,isSuccessful" | jq '.instanceOperation | {dtFinish, isSuccessful}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Полигон полностью повторяет эту последовательность: те же эндпоинты, те же поля, те же статус-коды (201 при создании, 409 при повторном run, 404 при несуществующем ресурсе).
|
||||||
|
|
||||||
|
### Валидация входных данных
|
||||||
|
|
||||||
|
- `serviceId` — только integer > 0 (строка/float/отрицательное → 400)
|
||||||
|
- Тело запроса — должно быть JSON-объектом (массив → 400)
|
||||||
|
- `svcOperationCfsParamId` — только integer (строка → 400)
|
||||||
|
- Повторный `run` — 409 (already completed)
|
||||||
|
|
||||||
|
### Аутентификация
|
||||||
|
|
||||||
|
- Основные эндпоинты — **без авторизации**
|
||||||
|
- `_mock/*` — заголовок `X-Mock-Auth` (если `MOCK_AUTH_TOKEN` задан в env)
|
||||||
|
- По умолчанию `MOCK_AUTH_TOKEN` не задан → `_mock/*` открыты
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Тесты
|
||||||
|
|
||||||
|
### test_api.py — 39 smoke-тестов
|
||||||
|
|
||||||
|
Запуск: `python3 tests/test_api.py`
|
||||||
|
|
||||||
|
Проверяет деплоенный полигон по всем 17 эндпоинтам:
|
||||||
|
- Health, services, instances (CRUD + pagination + edge cases)
|
||||||
|
- Operations (create → params → validate → run → double-run 409)
|
||||||
|
- Fail-next (one-shot error simulation)
|
||||||
|
- Auth (без токена, неверный токен, верный токен)
|
||||||
|
- Mock state (reset, state dump, delay bounds)
|
||||||
|
- Multi-stand (изоляция dev/test/prod, fail-next изоляция)
|
||||||
|
|
||||||
|
### fuzz_test.py — 147 фаззинг-тестов
|
||||||
|
|
||||||
|
Запуск: `cd site && PYTHONPATH=. python3 ../tests/fuzz_test.py` (локально, Flask test client)
|
||||||
|
|
||||||
|
Злонамеренные и экстремальные сценарии:
|
||||||
|
- SQL injection, XSS, Unicode-emoji, null-байты, 10000-символьные строки
|
||||||
|
- Неверные типы (строка вместо int, float, отрицательные)
|
||||||
|
- Отсутствующие поля, null-поля
|
||||||
|
- Path traversal, двойные слеши
|
||||||
|
- Быстрые повторы (10 инстансов подряд)
|
||||||
|
- Невалидные UUID
|
||||||
|
- Неправильные HTTP-методы (PUT на GET, DELETE на POST)
|
||||||
|
|
||||||
|
### compare_test.py — сравнение с реальным API
|
||||||
|
|
||||||
|
Запуск: `python3 tests/compare_test.py`
|
||||||
|
|
||||||
|
Сравнивает read-only эндпоинты полигона с реальным Nubes API (dev/test/prod):
|
||||||
|
- `GET /services` — одинаковый ли список сервисов
|
||||||
|
- `GET /services/{id}` — одинаковые ли операции (svcOperationId, operation, kind, action)
|
||||||
|
- `GET /instanceOperations/default/{id}` — одинаковые ли cfsParams
|
||||||
|
|
||||||
|
Классификация расхождений:
|
||||||
|
- 🔴 BUG — полигон неправ (лишний/неверный параметр)
|
||||||
|
- 🟡 LAG — реальный API обогнал (новый параметр, YAML устарел)
|
||||||
|
- 🟢 BETTER — полигон правильнее реального API (null→"string", HTML entities)
|
||||||
|
|
||||||
|
### Юнит-тесты
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_converter.py -v # 10 тестов from_stands.py
|
||||||
|
pytest tests/test_state_machine.py -v # 9 тестов apply_effect()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Переменные окружения
|
||||||
|
|
||||||
|
| Переменная | По умолчанию | Описание |
|
||||||
|
|-----------|-------------|----------|
|
||||||
|
| `POLYGON_STANDS` | (пусто) | Стенды: `dev:services/dev,test:services/test,prod:services/prod` |
|
||||||
|
| `MOCK_AUTH_TOKEN` | (пусто) | Токен для `_mock/*`. Если пусто — auth отключена |
|
||||||
|
| `MOCK_OP_DELAY` | `0.1` | Задержка операции в секундах |
|
||||||
|
| `POLYGON_ENDPOINT` | `https://polygon.pythonk8s.dev.nubes.ru` | URL для OpenAPI-спеке |
|
||||||
|
| `WEB_CONCURRENCY` | `1` | ⛔ НЕ менять — сломает изоляцию состояния |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Ограничения и известные проблемы
|
||||||
|
|
||||||
|
| Ограничение | Причина | Обход |
|
||||||
|
|------------|---------|-------|
|
||||||
|
| 1 gunicorn-воркер | Состояние в памяти | Не менять WEB_CONCURRENCY |
|
||||||
|
| Большие ответы (>20KB) обрываются | 1 воркер + сетевой таймаут | Swagger-спека встроена в HTML |
|
||||||
|
| Нет персистентности | Всё в памяти | Использовать `_mock/reset` в тестах |
|
||||||
|
| Auth не включена на проде | `MOCK_AUTH_TOKEN` не задан | Добавить в env деплоя |
|
||||||
|
| YAML обновляется вручную | Нет автотриггера | Запускать `from_stands.py` при изменении терраформа |
|
||||||
|
| Нет HTTPS-валидации сертификатов | Dev-инструмент | — |
|
||||||
|
| Swagger загружается с CDN (jsdelivr) | Нет офлайн-версии | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Деплой
|
||||||
|
|
||||||
|
Polygon — managed Flask на Nubes pythonk8s.
|
||||||
|
|
||||||
|
**Procfile:**
|
||||||
|
```
|
||||||
|
web: gunicorn site.app:app --bind 0.0.0.0:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Процесс редеплоя:**
|
||||||
|
1. Закоммитить и запушить изменения в `master`
|
||||||
|
2. В Nubes UI нажать «Redeploy»
|
||||||
|
3. Nubes: стягивает репу, билдит, запускает gunicorn на порту 8000
|
||||||
|
4. Healthcheck: `GET /health` → должен вернуть `OK`
|
||||||
|
|
||||||
|
**Важно:** YAML-конфиги (`services/`) скоммичены в git.
|
||||||
|
При редеплое Nubes клонирует репу → YAML уже на месте.
|
||||||
|
Не нужно запускать `from_stands.py` при деплое.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Чеклист для нового AI-агента
|
||||||
|
|
||||||
|
При старте нового чата прочитай:
|
||||||
|
1. Этот документ
|
||||||
|
2. `polygon/site/app.py` — точка входа
|
||||||
|
3. `polygon/site/config/loader.py` — загрузка конфигов
|
||||||
|
4. `polygon/site/routes/openapi.py` — OpenAPI-спека
|
||||||
|
5. `polygon/tests/test_api.py` — основной тест-сьют
|
||||||
|
|
||||||
|
Ключевые инварианты (НЕ нарушать):
|
||||||
|
- WEB_CONCURRENCY = 1 всегда
|
||||||
|
- `state` и `SERVICES` — через LocalProxy, не напрямую
|
||||||
|
- `_mock/*` требует X-Mock-Auth (если задан MOCK_AUTH_TOKEN)
|
||||||
|
- `serviceId` валидируется как int > 0
|
||||||
|
- YAML обновляется через `from_stands.py`, коммитится, редеплоится
|
||||||
@@ -90,3 +90,95 @@
|
|||||||
Я хочу чтобы в итоге получился **конкретный план действий**:
|
Я хочу чтобы в итоге получился **конкретный план действий**:
|
||||||
что тестировать, как часто, что считать ошибкой, что — допустимым
|
что тестировать, как часто, что считать ошибкой, что — допустимым
|
||||||
расхождением.
|
расхождением.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ответ Соннета (2026-08-02)
|
||||||
|
|
||||||
|
### 4. dataType: None — первопричина найдена
|
||||||
|
|
||||||
|
`dict.get(key, default)` возвращает `default` **только если ключ отсутствует**.
|
||||||
|
Если в YAML написано `data_type: null` — ключ *есть*, значение — `None`,
|
||||||
|
дефолт `"string"` не срабатывает.
|
||||||
|
|
||||||
|
### Мои ответы
|
||||||
|
|
||||||
|
**Q4.1 — data_type: null в YAML?** Проверил — в терраформ-YAML нет
|
||||||
|
`data_type: null`. Реальный API возвращает `dataType: null` для параметра
|
||||||
|
`nestedRefExample` (param 396). Полигон возвращает `"string"` — он ПРАВИЛЬНО
|
||||||
|
применяет дефолт там, где реальный API отдаёт null. Это не баг полигона,
|
||||||
|
а улучшение.
|
||||||
|
|
||||||
|
**Q4.2 — _convert_sub_params?** Та же уязвимость потенциально есть, но не
|
||||||
|
проявляется — sub_params всегда имеют data_type.
|
||||||
|
|
||||||
|
**Q1.1 — частота регенерации YAML?** ВРУЧНУЮ. `from_stands.py` запускается
|
||||||
|
человеком когда он вспомнит. Никакого cron/webhook.
|
||||||
|
|
||||||
|
**Q1.2 — лаг от реального API до YAML?** Непредсказуемо. От часов до недель.
|
||||||
|
Зависит от того когда кто-то запустит `from_stands.py`.
|
||||||
|
|
||||||
|
**Q1.3 — потребитель результатов?** Разработчик. Ему нужно знать «полигон
|
||||||
|
устарел, перегенери YAML», а не «полигон сломан».
|
||||||
|
|
||||||
|
**Q2.1 — дополнительные эндпоинты в реальном API?** Не проверял. Надо
|
||||||
|
сравнить полный список эндпоинтов.
|
||||||
|
|
||||||
|
**Q2.2 — lifecycle поля?** Не сравниваются в текущем compare_test.py. Надо
|
||||||
|
добавить.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ответ Соннета — раунд 2
|
||||||
|
|
||||||
|
### Три категории расхождений — 👍 принимаю
|
||||||
|
|
||||||
|
| Категория | Значение | Реакция |
|
||||||
|
|---|---|---|
|
||||||
|
| 🔴 REAL BUG | полигон ≠ реальный API, полигон неправ | FAIL |
|
||||||
|
| 🟡 LAG | новый параметр в реальном API, нет в полигоне | WARN |
|
||||||
|
| 🟢 POLYGON BETTER | реальный API отдаёт null/entities, полигон — правильно | INFO |
|
||||||
|
|
||||||
|
Для prod 🟡 LAG тоже должен быть заметен.
|
||||||
|
|
||||||
|
### Мои ответы — раунд 2
|
||||||
|
|
||||||
|
**Q5.1 — сервис есть в полигоне, пропал из реального API?**
|
||||||
|
Теоретически да — если сервис удалили из реального API, а terraform ещё
|
||||||
|
не обновили. Это 🔴 REAL BUG и должно быть FAIL. Полигон не должен
|
||||||
|
эмулировать несуществующие сервисы.
|
||||||
|
|
||||||
|
**Q5.2 — HTML-entities?**
|
||||||
|
Нормализовать при сравнении: `html.unescape()` для real API перед сравнением.
|
||||||
|
Считать 🟢 POLYGON BETTER, не ошибка.
|
||||||
|
|
||||||
|
**Q5.3 — lifecycle поля?**
|
||||||
|
Проверил — ни реальный API, ни полигон НЕ возвращают `lifecycle` в
|
||||||
|
`GET /services/{id}`. Сравнивать нечего, вопрос снят.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ответ Соннета — раунд 3 (финальный)
|
||||||
|
|
||||||
|
### Q6.1 — defaultValue, valueList и др.
|
||||||
|
|
||||||
|
Реальный API возвращает **29 полей** на каждый cfsParam: `defaultValue`,
|
||||||
|
`valueList`, `isModifiable`, `isRequired`, `isHidden`, `descr`, `man`,
|
||||||
|
`regex`, `maxlength`, `minvalue` и т.д. Полигон возвращает подмножество
|
||||||
|
из ~6-8 полей.
|
||||||
|
|
||||||
|
Сравнивать нужно только те поля, которые `from_stands.py` реально генерирует:
|
||||||
|
`defaultValue`, `valueList`, `isModifiable`, `isRequired`. Остальные либо
|
||||||
|
отсутствуют в терраформ-YAML, либо не имеют смысла для мока.
|
||||||
|
|
||||||
|
### Q6.2 — cfsParamsByOp
|
||||||
|
|
||||||
|
Это **внутренний индекс** полигона, не API-эндпоинт. Связь «какие параметры
|
||||||
|
к какой операции» уже проверяется через `GET /instanceOperations/default/{id}`
|
||||||
|
— если в ответе правильный набор параметров, значит cfsParamsByOp правильный.
|
||||||
|
Отдельно сравнивать не нужно.
|
||||||
|
|
||||||
|
### Q6.3 — формат вывода
|
||||||
|
|
||||||
|
stdout + exit code — достаточно. Разработчик запускает вручную, смотрит
|
||||||
|
глазами. Файл отчёта переусложнит. Если понадобится история — можно потом.
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Соннет: полный аудит Polygon v0.5.5 + Swagger + тесты
|
||||||
|
|
||||||
|
> Адресат: Claude Sonnet 4.6 (новый чат)
|
||||||
|
> ⛔ Только анализ и советы в чат. Не редактировать файлы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что такое Polygon
|
||||||
|
|
||||||
|
Эмулятор REST API облачной платформы Nubes для интеграционных тестов.
|
||||||
|
Задеплоен на `polygon.pythonk8s.dev.nubes.ru`. Flask 3.0 + gunicorn, 1 воркер.
|
||||||
|
|
||||||
|
**3 изолированных стенда:** dev (37 сервисов), test (37), prod (35).
|
||||||
|
Состояние в памяти, URL: `/dev/api/v1/svc/...`, `/test/...`, `/prod/...`.
|
||||||
|
В Swagger — выпадайка выбора стенда.
|
||||||
|
|
||||||
|
YAML-конфиги сервисов генерируются из терраформ-репы
|
||||||
|
(`~/tf_provider/generated/{dev,test,prod}/resources_yaml/`) через `from_stands.py`.
|
||||||
|
|
||||||
|
**Файлы для анализа:**
|
||||||
|
- `polygon/site/routes/openapi.py` — OpenAPI 3.1.0 спека (~500 строк)
|
||||||
|
- `polygon/site/templates/swagger.html` — Swagger UI 5
|
||||||
|
- `polygon/site/templates/index.html` — главная страница
|
||||||
|
- `polygon/site/static/style.css` — дизайн-система
|
||||||
|
- `polygon/site/routes/` — все роуты (7 blueprint'ов)
|
||||||
|
- `polygon/tests/test_api.py` — 35 smoke-тестов
|
||||||
|
- `polygon/tests/fuzz_test.py` — 147 фаззинг-тестов
|
||||||
|
- `polygon/tests/compare_test.py` — сравнение с реальным API
|
||||||
|
|
||||||
|
## Что уже сделано
|
||||||
|
|
||||||
|
- 17 эндпоинтов, полный CRUD инстансов и операций
|
||||||
|
- Аутентификация `X-Mock-Auth` для `_mock/*`
|
||||||
|
- Валидация serviceId (int > 0, не массив, не null)
|
||||||
|
- 35 smoke + 147 fuzz тестов — 0 реальных багов
|
||||||
|
- Сравнение с реальным API: YAML ↔ API — 0 расхождений
|
||||||
|
|
||||||
|
## Что нужно от тебя
|
||||||
|
|
||||||
|
### 1. Swagger/OpenAPI
|
||||||
|
|
||||||
|
Открой `polygon/site/routes/openapi.py` и `polygon/site/templates/swagger.html`.
|
||||||
|
Проанализируй:
|
||||||
|
|
||||||
|
- Полнота схем — все ли поля ответов описаны?
|
||||||
|
- Правильные ли status codes (200/201/400/404/409)?
|
||||||
|
- Удобство Try it out — example'ы, enum'ы, default'ы
|
||||||
|
- Группировка тегов — логично ли?
|
||||||
|
- Авторизация в Swagger UI — правильно ли работает?
|
||||||
|
- Нет ли лишнего или недостающего?
|
||||||
|
- Русские описания — понятны ли, не слишком ли длинные?
|
||||||
|
|
||||||
|
### 2. Сравнительное тестирование
|
||||||
|
|
||||||
|
У нас есть 3 read-only эндпоинта для сравнения:
|
||||||
|
- `GET /services`
|
||||||
|
- `GET /services/{id}`
|
||||||
|
- `GET /instanceOperations/default/{id}`
|
||||||
|
|
||||||
|
Какие ещё эндпоинты можно безопасно сравнивать с реальным API?
|
||||||
|
Что ещё можно проверить не делая мутирующих запросов?
|
||||||
|
|
||||||
|
### 3. Дополнительные тесты
|
||||||
|
|
||||||
|
Что мы упустили? Какие сценарии, краевые случаи, негативные тесты
|
||||||
|
стоит добавить? В том числе:
|
||||||
|
- Тесты через Swagger UI (браузерные)
|
||||||
|
- Нагрузочные/параллельные
|
||||||
|
- Специфичные для отдельных сервисов
|
||||||
|
- Тесты на совместимость с app-autotest
|
||||||
|
|
||||||
|
### 4. Замечания по коду/архитектуре
|
||||||
|
|
||||||
|
Что можно улучшить не переписывая всё? Любые баги, уязвимости,
|
||||||
|
потенциальные проблемы которые ты видишь.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Формат: свободный. Главное — **конкретные советы** с указанием что и где
|
||||||
|
менять, а не общие рассуждения.
|
||||||
Reference in New Issue
Block a user