Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c64812b039 | ||
|
|
18d08ccccd | ||
|
|
cfc341db44 | ||
|
|
333a3d5e63 | ||
|
|
4ee0433e28 | ||
|
|
85b02c70f8 | ||
|
|
c31cdb73ef | ||
|
|
ef13e52c80 | ||
|
|
ede5b68b74 | ||
|
|
9fb21a40a8 | ||
|
|
5dff1eb796 | ||
|
|
e9b3e6d0dc | ||
|
|
07da0992aa | ||
|
|
ad8cc0205e | ||
|
|
c0d908d176 | ||
|
|
bddbdfb63b | ||
|
|
f470b79ca1 | ||
|
|
f698919dce | ||
|
|
dde6636372 | ||
|
|
76b067604b | ||
|
|
a42f5b335f | ||
|
|
b0f051344d | ||
|
|
de6217e07b | ||
|
|
6978d13d0e | ||
|
|
633537a775 | ||
|
|
28b2519fda | ||
|
|
7dd62c365a | ||
|
|
4b3d436d0d | ||
|
|
42e3ca1ab3 | ||
|
|
fd55c50753 | ||
|
|
bcf9f00654 | ||
|
|
bb15455bcb | ||
|
|
acf6a575b4 | ||
|
|
09d663d6d9 | ||
|
|
d0f1edf401 | ||
|
|
5eb7496357 | ||
|
|
89aa5268e7 | ||
|
|
c7b1f4d459 | ||
|
|
28dee4632a |
@@ -1,80 +1,25 @@
|
|||||||
# ⛔ ПРАВИЛА ПРОЕКТА AUTOTEST
|
# Правила (читают ВСЕ агенты — Опус, Соннет, Fable, DeepSeek)
|
||||||
|
|
||||||
## ⛔ ЗАПРЕТ НА sed .... ПОЛНЫЙ ЗАПРЕТ НА SED
|
## ⛔ БЕЗ «ДЕЛАЙ» — НИЧЕГО НЕ ДЕЛАТЬ
|
||||||
Только `replace_string_in_file` или `create_file`. sed не использовать НИКОГДА.
|
Ни кода, ни git, ни curl, ни терминала, ни kubectl. Только смотреть и отвечать словами.
|
||||||
НИКОГДА НЕ ИСПОЛЬЗОВАТЬ SED !!!!!
|
Ждать явного «делай», «да», «пушь», «коммитить». НЕТ «делай» — НЕТ действий.
|
||||||
|
|
||||||
|
## ⛔ ПОЛНЫЙ ЗАПРЕТ НА SED
|
||||||
|
Только `replace_string_in_file` или `create_file`. sed не использовать НИКОГДА.
|
||||||
|
|
||||||
|
## ⛔ АБСОЛЮТНЫЕ ПУТИ ВЕЗДЕ
|
||||||
|
От корня `/home/naeel/nubes/autotest/`. Никаких относительных.
|
||||||
|
|
||||||
## ⛔⛔⛔ «ГОВОРИ» — ТОЛЬКО ПОНЯТЬ И ЖДАТЬ
|
## ⛔ ТЕРМИНАЛ — С ТАЙМАУТАМИ
|
||||||
Если пользователь пишет **«говори»** — это значит:
|
curl: `--max-time N`, ssh: `-o ConnectTimeout=N`.
|
||||||
1. Уяснить что он имел в виду
|
|
||||||
2. Вывести в чат: ЧТО понял, КАК понял, ПЛАН действий
|
|
||||||
3. **ЖДАТЬ.** Никаких действий.
|
|
||||||
4. Только после **«делай»** — приступать.
|
|
||||||
ПРЕЦЕДЕНТ 2026-07-26: пользователь сказал «сначала скажи что понял» — AI должен был только объяснить понимание и ждать.
|
|
||||||
|
|
||||||
## ⛔⛔⛔⛔⛔ АБСОЛЮТНЫЙ ЗАПРЕТ НА ЛЮБЫЕ ДЕЙСТВИЯ БЕЗ «ДЕЛАЙ»
|
## ⛔ НЕ УБИВАТЬ ПРОЦЕССЫ БЕЗ РАЗРЕШЕНИЯ
|
||||||
**НИЧЕГО не делать без явной команды «делай». ВООБЩЕ НИЧЕГО.**
|
|
||||||
Ни писать код, ни править код, ни исправлять ошибки, ни git, ни curl, ни терминал.
|
|
||||||
ТОЛЬКО отвечать на вопросы словами. Всё.
|
|
||||||
ПРЕЦЕДЕНТ 2026-07-25: AI самовольно добавил «все сервисы» в UI без команды.
|
|
||||||
ПРЕЦЕДЕНТ 2026-07-25: AI сказал «исправляю» и полез править без «делай».
|
|
||||||
|
|
||||||
## ⛔⛔⛔ НИКОГДА НЕ «УЛУЧШАТЬ» БЕЗ ПРЯМОЙ КОМАНДЫ
|
## ✅ ПОСЛЕ ПРАВКИ — ПРОВЕРИТЬ
|
||||||
**Запрещено «заодно улучшить», «раз уж меняю», «добавить заодно» и прочая самодеятельность.**
|
`python3 -m py_compile файл.py` для .py, `node -c файл.js` для .js.
|
||||||
Делать ТОЛЬКО то что прямо сказано. Никаких попутных изменений.
|
|
||||||
ПРЕЦЕДЕНТ 2026-07-25: AI менял отображение инстансов и «заодно» показал все сервисы.
|
|
||||||
ПРЕЦЕДЕНТ 2026-07-25: AI «заодно» добавил кнопку выхода в другом проекте.
|
|
||||||
|
|
||||||
## ⛔⛔⛔ БЕЗ «ДЕЛАЙ» — НИЧЕГО НЕ ДЕЛАТЬ
|
|
||||||
Ни кода, ни git, ни терминала, ни curl, ни kubectl. Вообще ничего.
|
|
||||||
Ждать явного «делай», «да», «пушь», «пушить», «коммитить».
|
|
||||||
НЕТ «делай» — НЕТ действий. Только смотреть и отвечать.
|
|
||||||
|
|
||||||
## ⛔⛔⛔ ВОПРОС — ТОЛЬКО ОТВЕТ
|
|
||||||
Любой вопрос в любой форме («???», «поясни», «как», «почему», «где») — ТОЛЬКО ОТВЕТ словами.
|
|
||||||
НИКАКИХ действий. НИКАКИХ «делать?», «пуш?», «проверить?».
|
|
||||||
|
|
||||||
## ⛔⛔⛔ НИКОГДА НЕ ТОРОПИТЬСЯ
|
|
||||||
Сначала ВСЁ обдумать. Проверить. Перепроверить. Только потом отвечать.
|
|
||||||
|
|
||||||
## ⛔⛔⛔ ПРЕДУПРЕЖДАТЬ ОБ ОПАСНОСТИ
|
|
||||||
Если команда может сломать, удалить, изменить состояние сервиса — **СНАЧАЛА предупредить**, потом делать.
|
|
||||||
ПРЕЦЕДЕНТ 2026-07-23: `kubectl delete deploy` вместо API-редеплоя — удалил деплоймент, сервис упал.
|
|
||||||
|
|
||||||
## ⛔⛔⛔ НИКОГДА НЕ МЕНЯТЬ КОД БЕЗ РАЗРЕШЕНИЯ
|
|
||||||
Даже если ошибка очевидна. Даже если «исправление в одну строку».
|
|
||||||
Показать проблему → описать решение → ЖДАТЬ «делай».
|
|
||||||
|
|
||||||
## ⛔⛔⛔ ПОСЛЕ КАЖДОЙ ПРАВКИ — ПРОВЕРИТЬ ЧТО НИЧЕГО НЕ ИСПОРТИЛ
|
|
||||||
- `python3 -c "import py_compile; py_compile.compile('файл.py', doraise=True)"` — синтаксис Python
|
|
||||||
- `node -c файл.js` — синтаксис JS
|
|
||||||
- `grep -n "def имя_функции" файл.py` — все ли функции на месте
|
|
||||||
- После `replace_string_in_file` — прочитать соседние строки, не затёр ли соседнюю функцию
|
|
||||||
|
|
||||||
## ⛔ АБСОЛЮТНЫЕ ПУТИ — ВСЕГДА
|
|
||||||
Все пути в описаниях, планах, чате — от корня ФС.
|
|
||||||
❌ `STANDS/dev/...` → ✅ `/home/naeel/nubes/autotest/STANDS/dev/...`
|
|
||||||
|
|
||||||
|
|
||||||
## ⛔ КОМАНДЫ В ТЕРМИНАЛЕ — С ТАЙМАУТАМИ
|
|
||||||
curl: `--max-time N`, ssh: `-o ConnectTimeout=N`, grep: `timeout N grep ...`
|
|
||||||
|
|
||||||
## ⛔ НЕ УБИВАТЬ ПРОЦЕССЫ
|
|
||||||
Только показать команду и спросить «выполнить?».
|
|
||||||
|
|
||||||
## ✅ КОММИТ + ПУШ ПОСЛЕ КАЖДОЙ ПРАВКИ
|
## ✅ КОММИТ + ПУШ ПОСЛЕ КАЖДОЙ ПРАВКИ
|
||||||
После ЛЮБОГО изменения — сразу git add + commit + push. Не откладывать.
|
Сразу git add + commit + push. Не откладывать.
|
||||||
|
|
||||||
## 📝 ДОКУМЕНТИРОВАТЬ ВСЁ В HISTORY
|
## ✅ ДОКУМЕНТИРОВАТЬ В HISTORY
|
||||||
Всё что обсуждаем и делаем — записывать в `/home/naeel/nubes/autotest/HISTORY/`.
|
`/home/naeel/nubes/autotest/HISTORY/YYYY-MM-DD-session.md`. Commit + push после записи.
|
||||||
Формат: `YYYY-MM-DD-session-N.md`. Одна запись на сессию.
|
|
||||||
После каждой записи — commit + push.
|
|
||||||
|
|
||||||
## 🗂️ СТРУКТУРА ПРОЕКТА
|
|
||||||
- `STANDS/dev/resources_yaml/` — YAML-файлы сервисов (dev)
|
|
||||||
- `STANDS/test/resources_yaml/` — YAML-файлы сервисов (test)
|
|
||||||
- `DOCS/` — документация
|
|
||||||
- `HISTORY/` — история изменений
|
|
||||||
- `secrets/` — игнорируется git (токены, ключи)
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ secrets/*
|
|||||||
|
|
||||||
# Submodules / nested repos
|
# Submodules / nested repos
|
||||||
app-autotest/
|
app-autotest/
|
||||||
|
polygon/
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -19,6 +20,7 @@ Thumbs.db
|
|||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.venv/
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
# Go (embed.go generates from yaml)
|
# Go (embed.go generates from yaml)
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# AGENT BRIEFING — app-autotest (2026-07-31, v1.2.23)
|
||||||
|
|
||||||
|
> **Контекст для нового агента.** Прочитай этот файл полностью — он заменит тебе чтение 4166 строк истории чата. Здесь всё что нужно знать чтобы продолжить работу.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что такое app-autotest
|
||||||
|
|
||||||
|
Flask 3.1 + vanilla JS + PostgreSQL (psycopg2). Веб-приложение для автоматического тестирования сервисов облачной платформы Nubes через REST API.
|
||||||
|
|
||||||
|
**Ручной режим:** выбрать сервис → выбрать операцию (create/modify/delete/suspend/resume/redeploy) → заполнить параметры → запустить → поллинг статуса.
|
||||||
|
|
||||||
|
**Сценарный режим:** последовательность операций с передачей контекста между шагами. Сценарии редактируются через модальный UI, хранятся в БД.
|
||||||
|
|
||||||
|
**Деплой:** Nubes pythonk8s managed service, gunicorn --workers 2.
|
||||||
|
**URL:** `https://atest.pythonk8s.dev.nubes.ru`
|
||||||
|
**Репозиторий:** `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git` (ветка `master`)
|
||||||
|
|
||||||
|
### Файлы которые надо прочитать (обязательно)
|
||||||
|
|
||||||
|
- [DOCS/ARCHITECTURE.md](DOCS/ARCHITECTURE.md) — архитектура, эндпоинты, безопасность
|
||||||
|
- [DOCS/polygon-plan.md](DOCS/polygon-plan.md) — полный план мок-полигона
|
||||||
|
- [HISTORY/2026-07-31-session.md](HISTORY/2026-07-31-session.md) — хронология всего что сделано сегодня
|
||||||
|
- [TASKS/mock-architecture-prompt.md](TASKS/mock-architecture-prompt.md) — исходный промпт для Опуса
|
||||||
|
- [TASKS/universal-mock-generator-prompt.md](TASKS/universal-mock-generator-prompt.md) — промпт про STANDS YAML
|
||||||
|
|
||||||
|
### Структура кода
|
||||||
|
|
||||||
|
```
|
||||||
|
app-autotest/site/
|
||||||
|
├── app.py # точка входа (VERSION = "1.2.23")
|
||||||
|
├── api/
|
||||||
|
│ ├── auth.py # токен/clientId/stand из JWT cookie
|
||||||
|
│ ├── http_client.py # HttpClient (requests.Session) + автостенд
|
||||||
|
│ └── utils.py # find_uid(), uid_from_location()
|
||||||
|
├── operations/
|
||||||
|
│ ├── executor.py # ЕДИНЫЙ запуск (create→instanceOperations→params→run)
|
||||||
|
│ ├── poll.py # poll_until_done() — поллинг до dtFinish
|
||||||
|
│ ├── scenario.py # run_scenario() — шаги с резолвингом instance_ref
|
||||||
|
│ ├── terraform.py # send_params_terraform() — нормализация + refSvc + validate-cfs
|
||||||
|
│ ├── tracker.py # JSON-файловый кеш /tmp/instances-*.json (flock)
|
||||||
|
│ ├── get_instances.py # GET /instances с пагинацией
|
||||||
|
│ ├── get_params.py # параметры с ТЕКУЩИМИ значениями из state.params
|
||||||
|
│ ├── get_services.py # GET /services
|
||||||
|
│ └── service_list.py # services_{stand}.txt фильтр
|
||||||
|
├── db/
|
||||||
|
│ ├── pool.py # ThreadedConnectionPool(1,5)
|
||||||
|
│ ├── init_db.py # CREATE TABLE + миграции + idx_one_running + seed
|
||||||
|
│ ├── save_run.py # INSERT в runs + instance_meta JSONB
|
||||||
|
│ └── scenario_defs.py # CRUD scenario_definitions + lock_check (трёхсостояночный)
|
||||||
|
├── routes/
|
||||||
|
│ ├── main.py # GET/POST / — главная страница
|
||||||
|
│ ├── api_test.py # /api/test, /api/params, /api/log (основная логика)
|
||||||
|
│ ├── api_scenario_run.py # /api/scenario/run, /api/scenario/status
|
||||||
|
│ ├── api_scenario_defs.py # CRUD /api/scenario/definitions
|
||||||
|
│ ├── api_scenario.py # ДУБЛИКАТ? (241 строка)
|
||||||
|
│ └── api.py # LEGACY /api/run
|
||||||
|
├── static/
|
||||||
|
│ ├── app.js, style.css
|
||||||
|
│ └── js/ (12 файлов: utils, icons, snackbar, views, instances, operations,
|
||||||
|
│ params-render, history, scenario-list, scenario-form,
|
||||||
|
│ scenario-create, scenario-edit, scenario-delete)
|
||||||
|
└── templates/
|
||||||
|
└── index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Что было сделано сегодня (2026-07-31)
|
||||||
|
|
||||||
|
### Версии
|
||||||
|
- v1.2.16 → v1.2.23 (8 версий)
|
||||||
|
- 23 коммита в app-autotest, 9 в root autotest
|
||||||
|
|
||||||
|
### Комментарии ко ВСЕМУ коду
|
||||||
|
29 файлов, ~6000 строк. Подробные комментарии к каждой функции, каждому if-ветвлению, каждому архитектурному решению. Python + JS.
|
||||||
|
|
||||||
|
### Аудит безопасности (GPT-5.3-Codex, 3 раунда)
|
||||||
|
|
||||||
|
**Раунд 1 (11 находок):**
|
||||||
|
- v1.2.19: XSS params, JS injection onclick, polling timeout, has_target, tracker atomic update
|
||||||
|
- v1.2.20: _op_results lock (threading.Lock), advisory lock (pg_try_advisory_lock), _ensure_schema logging, stale async generation token, validate-cfs JSONDecodeError
|
||||||
|
|
||||||
|
**Раунд 2 (Codex нашёл 2 критических ошибки в моих фиксах):**
|
||||||
|
- ❌ Advisory lock сломан: брал lock на conn1, unlock на conn2 (другая сессия из пула)
|
||||||
|
- ❌ escName без `"` escape: HTML-атрибут onclick="..." разрывается
|
||||||
|
- v1.2.21: advisory lock → partial unique index `idx_one_running`, escName + `"`
|
||||||
|
|
||||||
|
**Раунд 3 (3 находки):**
|
||||||
|
- v1.2.22: UniqueViolation→409, escName + `&`, lock_check fallback
|
||||||
|
- v1.2.23: трёхсостояночный lock_check (True/False/None→503) после дискуссии
|
||||||
|
|
||||||
|
**Тесты:** `app-autotest/tests/` — 5 файлов от Codex (conftest, test_api_scenario_run, test_db_scenario_defs, test_static_regressions, README). Покрывают критические фиксы. НЕ запускались.
|
||||||
|
|
||||||
|
### Архитектура мок-полигона (Опус)
|
||||||
|
|
||||||
|
**Концепция:** отдельный managed-сервис `https://nubes_polygon.pythonk8s.dev.nubes.ru`, притворяющийся Nubes API для всех 37 сервисов.
|
||||||
|
|
||||||
|
**10 архитектурных решений** (см. [DOCS/polygon-plan.md](DOCS/polygon-plan.md)):
|
||||||
|
- Отдельный процесс :5001 (не blueprint)
|
||||||
|
- Data-driven: сервисы из STANDS YAML, не хардкод
|
||||||
|
- Ленивый dtFinish (без потоков)
|
||||||
|
- Единая стейт-машина (create→running→suspended→deleted)
|
||||||
|
- Реальный мерж params при modify
|
||||||
|
- `/_mock/reset` для тестов
|
||||||
|
|
||||||
|
**Универсальный конвертер STANDS YAML** — Опус подтвердил: `polygon/from_stands.py` читает 37 YAML из `STANDS/test/resources_yaml/` и генерит конфиги для всех сервисов. Маппинг почти 1:1 (см. HISTORY).
|
||||||
|
|
||||||
|
**Subresource-операции** — `create_user`/`create_database` через обычный `apply_effect` с флагом `subresource`. Универсально, без сервис-специфичного кода.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Что надо делать дальше
|
||||||
|
|
||||||
|
### 🔴 Срочно (сегодня)
|
||||||
|
|
||||||
|
**Реализовать мок-полигон** по плану в [DOCS/polygon-plan.md](DOCS/polygon-plan.md):
|
||||||
|
|
||||||
|
Фаза 1 — MVP:
|
||||||
|
1. `polygon/defaults.py` — `default_for(dataType)`
|
||||||
|
2. `polygon/from_stands.py` — конвертер STANDS YAML → polygon config
|
||||||
|
3. `polygon/state.py` — `MockState` (instances, operations, ленивый dtFinish, apply_effect, reset)
|
||||||
|
4. `polygon/server.py` — Flask на порту 5001, префикс `/api/v1/svc`, Location-заголовки
|
||||||
|
5. `polygon/services/` — НЕ создавать вручную! Генерится из STANDS
|
||||||
|
6. Интеграция в `auth.py` — short-circuit localhost (см. §3.3 в polygon-plan.md)
|
||||||
|
|
||||||
|
Фаза 2 — полный CRUD:
|
||||||
|
7. GET /instances, GET /instances/{uid}, GET /services, GET /services/{id}
|
||||||
|
8. GET /instanceOperations/default/{id} (cfsParams шаблон)
|
||||||
|
9. apply_effect для всех операций + subresource
|
||||||
|
10. `/_mock/reset`
|
||||||
|
|
||||||
|
Фаза 3 — тесты:
|
||||||
|
11. `tests/conftest.py` — фикстура поднятия мока
|
||||||
|
12. `tests/test_mock_integration.py` — 5 сценариев через app_client
|
||||||
|
|
||||||
|
### 🟡 В планах
|
||||||
|
|
||||||
|
- Прогнать конвертер на ВСЕХ 37 YAML, найти аномалии
|
||||||
|
- Исправить `instances.js:78` — instanceUid в onclick (низкий риск)
|
||||||
|
- Вынести `_op_results` в Redis/БД (архитектурное ограничение)
|
||||||
|
- Context snapshot всех инстансов пользователя (идея из HISTORY)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Ключевые файлы документации
|
||||||
|
|
||||||
|
| Файл | Содержание |
|
||||||
|
|------|-----------|
|
||||||
|
| [DOCS/ARCHITECTURE.md](DOCS/ARCHITECTURE.md) | Полная архитектура, эндпоинты Nubes API, безопасность (§8) |
|
||||||
|
| [DOCS/polygon-plan.md](DOCS/polygon-plan.md) | План мок-полигона: 3 фазы, 12 шагов, YAML-спецификация |
|
||||||
|
| [DOCS/architecture-final.md](DOCS/architecture-final.md) | Финальная архитектура |
|
||||||
|
| [HISTORY/2026-07-31-session.md](HISTORY/2026-07-31-session.md) | Хронология: все версии, аудит, ошибки, дискуссии |
|
||||||
|
| [TASKS/mock-architecture-prompt.md](TASKS/mock-architecture-prompt.md) | Исходный промпт для Опуса |
|
||||||
|
| [TASKS/universal-mock-generator-prompt.md](TASKS/universal-mock-generator-prompt.md) | Промпт про STANDS YAML + ответы |
|
||||||
|
| [STANDS/test/resources_yaml/](STANDS/test/resources_yaml/) | 37 YAML — источник для конвертера |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Что НЕ надо делать
|
||||||
|
|
||||||
|
- ❌ Не деплоить app-autotest без повышения версии
|
||||||
|
- ❌ Не править код без явного «делай»
|
||||||
|
- ❌ Не использовать sed для правки файлов
|
||||||
|
- ❌ Не удалять файлы/БД/контейнеры без разрешения
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Как запускать
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# app-autotest (основное)
|
||||||
|
cd app-autotest/site && python app.py # порт 5000
|
||||||
|
|
||||||
|
# Мок-полигон (когда будет готов)
|
||||||
|
cd app-autotest && STANDS_DIR=../STANDS/test/resources_yaml python polygon/server.py # порт 5001
|
||||||
|
|
||||||
|
# Тесты (когда будут готовы)
|
||||||
|
NUBES_API_ENDPOINT=http://localhost:5001/api/v1/svc pytest tests/ -v
|
||||||
|
```
|
||||||
@@ -188,3 +188,43 @@ SVC_ID = 1 — фиксированный сервис (Болванка)
|
|||||||
- Gunicorn multi-worker → общие данные через файлы + flock
|
- Gunicorn multi-worker → общие данные через файлы + flock
|
||||||
- `/tmp/` теряется при редеплое
|
- `/tmp/` теряется при редеплое
|
||||||
- User-Agent: `Mozilla/5.0` обязателен (DDoS-Guard)
|
- User-Agent: `Mozilla/5.0` обязателен (DDoS-Guard)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Безопасность и конкуренция (аудит GPT-5.3-Codex, 2026-07-31)
|
||||||
|
|
||||||
|
Полный аудит 29 файлов (~6000 строк Python + vanilla JS). Исправлено в v1.2.19-v1.2.20.
|
||||||
|
|
||||||
|
### Защита от XSS (Frontend)
|
||||||
|
|
||||||
|
- **`_esc(s)` в utils.js** — HTML-escape: `&` → `&`, `"` → `"`, `<` → `<`
|
||||||
|
Применяется ко ВСЕМ данным из API перед `innerHTML`.
|
||||||
|
- **params в scenario-list.js** — `_esc(k)+'='+_esc(v)` (было `k+'='+v` без экранирования).
|
||||||
|
- **JS injection в onclick** — имена сценариев с `'` теперь `replace(/'/g, "\\'")` перед
|
||||||
|
вставкой в JS-строку внутри HTML-атрибута.
|
||||||
|
|
||||||
|
### Защита от гонок (Backend)
|
||||||
|
|
||||||
|
- **`_op_results`** — `threading.Lock()` вокруг всех операций чтения/записи/cleanup.
|
||||||
|
`pop(k, None)` вместо `del dict[k]` — безопасно при конкурентном доступе.
|
||||||
|
- **Partial unique index для сценариев** — `CREATE UNIQUE INDEX idx_one_running
|
||||||
|
ON scenario_runs (client_id, stand) WHERE status = 'RUNNING'`.
|
||||||
|
Делает `INSERT INTO scenario_runs ... status='RUNNING'` атомарной проверкой:
|
||||||
|
вторая параллельная вставка получает unique violation → 409.
|
||||||
|
Это заменило сломанную реализацию на `pg_try_advisory_lock` (v1.2.20),
|
||||||
|
где lock брался на одном соединении, а unlock — на другом (из пула).
|
||||||
|
- **Tracker** — `_atomic_update()`: read→mutate→write под одним `fcntl.flock`.
|
||||||
|
Исключает lost-update между `add()` и `remove()` из разных воркеров gunicorn.
|
||||||
|
|
||||||
|
### Защита от зависания (Frontend polling)
|
||||||
|
|
||||||
|
- **Счётчик ошибок** в `scenarioPollTimer` — после 5 последовательных ошибок:
|
||||||
|
`stopScenarioPoll()` + `busy=false` + сообщение об ошибке.
|
||||||
|
- **Generation token** в `scenario-form.js` — `_renderGen` предотвращает перезапись
|
||||||
|
нового DOM старыми данными от async `loadStepParams()`.
|
||||||
|
|
||||||
|
### Известные ограничения
|
||||||
|
|
||||||
|
- **`_op_results` in-memory на воркер** — не shared между gunicorn-воркерами.
|
||||||
|
При отсутствии stickiness статус может читаться из API fallback вместо кеша.
|
||||||
|
Решение (отложено): Redis или общая таблица в БД для статусов операций.
|
||||||
|
|||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
# DOCS — Архив документации
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `ARCHITECTURE.md` | Актуальная архитектура: эндпоинты, схема БД, безопасность (§8 с фиксами аудита) |
|
||||||
|
| `ARCHITECTURE-FULL.md` | Развёрнутая архитектура: полный список файлов, все роуты, зависимости |
|
||||||
|
| `ARCHITECTURE.legacy.md` | Архитектура v1.0.x (до рефакторинга) |
|
||||||
|
| `architecture-final.md` | Финальная архитектура после всех рефакторингов и аудитов |
|
||||||
|
| `architecture-next.md` | Черновик архитектуры vNext — идеи на будущее |
|
||||||
|
| `architecture-questions-for-sonnet.md` | Вопросы к Sonnet по архитектуре (перед ревью) |
|
||||||
|
| `architecture-review-sonnet.md` | Ревью архитектуры от Sonnet |
|
||||||
|
| `architecture-round2.md` | Второй раунд архитектурных решений |
|
||||||
|
|
||||||
|
## Как делать (How-to)
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `howto-flask-nubes.md` | **ВАЖНО.** Как писать Flask-приложение под Nubes: структура `site/`, порт 5000, `app.run()`, запреты |
|
||||||
|
|
||||||
|
## API Nubes
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `api-access.md` | Как получить доступ к Nubes API: токены, clientId, стенды |
|
||||||
|
| `api-create-flow.md` | Полный flow создания инстанса: instances → instanceOperations → params → run → poll |
|
||||||
|
| `api-operation-stages.md` | Стадии выполнения операций (plan, apply, etc.) |
|
||||||
|
| `terraform-operations-full-logic.md` | Логика Terraform-операций: validate-cfs, refSvc, нормализация параметров |
|
||||||
|
|
||||||
|
## Планы
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `polygon-plan.md` | **ВАЖНО.** План мок-полигона: 3 фазы, 12 шагов, YAML-спецификация |
|
||||||
|
| `opus-plan-2026-07-31.md` | План Опуса: унификация executor, гибкие ссылки, 4 фазы 13 шагов |
|
||||||
|
| `test-results-history-plan.md` | План истории результатов тестов |
|
||||||
|
| `gpt56-sol-plan.md` | План Sol по GPT-5.6 |
|
||||||
|
|
||||||
|
## Промпты для агентов
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `agent-analysis-request.md` | Запрос на анализ проекта (архитектура, риски, рекомендации) |
|
||||||
|
| `opus-architecture-prompt.md` | Промпт Опусу: полное описание проекта + вопросы по архитектуре |
|
||||||
|
| `opus-frontend-prompt.md` | Промпт Опусу: вопросы по фронтенду (instance dropdown) |
|
||||||
|
| `gpt56-sol-prompt.md` | Промпт GPT-5.6: описание проекта для Sol |
|
||||||
|
| `sonnet-review-prompt.md` | Промпт Sonnet для code review |
|
||||||
|
|
||||||
|
## Ревью и аудиты — Sonnet
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `sonnet-full-code-review.md` | Полный code review всех файлов |
|
||||||
|
| `sonnet-full-response.md` | Полный ответ Sonnet на review |
|
||||||
|
| `sonnet-full-review.md` | Сводка review |
|
||||||
|
| `sonnet-architecture-review-v1.0.89.md` | Архитектурное ревью v1.0.89 |
|
||||||
|
| `sonnet-architecture-review-v1.0.89-r2.md` | Второй раунд архитектурного ревью |
|
||||||
|
| `sonnet-review-v1.1.11.md` | Ревью v1.1.11 |
|
||||||
|
| `sonnet-review-answers.md` | Ответы на замечания Sonnet |
|
||||||
|
| `sonnet-review-round2.md` | Второй раунд ревью |
|
||||||
|
| `sonnet-review-round3.md` | Третий раунд ревью |
|
||||||
|
| `sonnet-final-audit.md` | Финальный аудит |
|
||||||
|
| `sonnet-final-review.md` | Финальное ревью |
|
||||||
|
| `sonnet-missed-bugs.md` | Пропущенные баги (что Sonnet не нашёл) |
|
||||||
|
| `sonnet-params-and-audit.md` | Параметры + аудит |
|
||||||
|
| `sonnet-response-params-audit.md` | Ответ: параметры и аудит |
|
||||||
|
| `sonnet-response-final.md` | Финальный ответ |
|
||||||
|
| `sonnet-response-round2.md` | Ответ второго раунда |
|
||||||
|
| `sonnet-response-v1.1.11.md` | Ответ на ревью v1.1.11 |
|
||||||
|
| `sonnet-response-serviceInstanceUid.md` | Ответ про serviceInstanceUid |
|
||||||
|
| `sonnet-response-state-out.md` | Ответ про state.out |
|
||||||
|
| `sonnet-state-out-valuelist.md` | stateOut + valueList |
|
||||||
|
| `sonnet-terraform-diff.md` | Разбор Terraform diff |
|
||||||
|
| `sonnet-new-chat.md` | Новый чат с Sonnet |
|
||||||
|
| `sonnet-question-tracker.md` | Трекер вопросов к Sonnet |
|
||||||
|
|
||||||
|
## Ревью — Опус
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `opus-questions-2026-07-31.md` | 19 уточняющих вопросов Опуса по архитектуре |
|
||||||
|
| `opus-instance-dropdown-questions.md` | Вопросы Опуса про instance dropdown |
|
||||||
|
| `opus-review-v1.2.0.md` | Ревью v1.2.0 от Опуса |
|
||||||
|
|
||||||
|
## Вопросы-ответы
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `questions-to-sol.md` | Вопросы к Sol |
|
||||||
|
| `sol-answers.md` | Ответы Sol |
|
||||||
|
| `sol-scenario-editor.md` | Sol про редактор сценариев |
|
||||||
|
|
||||||
|
## Прочее
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `HISTORY.md` | Краткая хронология версий (v1.0.x → v1.2.x) |
|
||||||
|
| `dialogues.md` | Диалоги/дискуссии в процессе разработки |
|
||||||
|
| `service-categories.md` | Категории сервисов Nubes (37 сервисов, их типы) |
|
||||||
|
| `review-comparison-sonnet-opus.md` | Сравнение ревью Sonnet vs Opus |
|
||||||
|
| `step-by-step-audit-v1.0.50.md` | Пошаговый аудит v1.0.50 |
|
||||||
|
| `vm-213.md` | Заметки о VM-213 (тестовый стенд) |
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Архитектура app-autotest — рефакторинг (2026-07-31)
|
||||||
|
|
||||||
|
> План: `DOCS/opus-plan-2026-07-31.md` | Ревью: `DOCS/opus-review-v1.2.0.md`
|
||||||
|
> Текущая версия: **v1.2.1** ✅
|
||||||
|
|
||||||
|
## ✅ Реализовано (Фазы 1-3)
|
||||||
|
|
||||||
|
### Новые модули
|
||||||
|
- `api/utils.py` — find_uid(), uid_from_location() (вместо 2 дублей)
|
||||||
|
- `operations/poll.py` — poll_until_done() (общий поллинг)
|
||||||
|
- `operations/executor.py` — execute_operation() (единый CREATE/non-CREATE флоу, tracker_add внутри)
|
||||||
|
|
||||||
|
### Рефакторинг
|
||||||
|
- `api_test.py` — через executor + poll_until_done, CMDB delete отдельно
|
||||||
|
- `scenario.py` — через executor + poll, output/ref/bindings резолвинг
|
||||||
|
- `api_scenario_defs.py` — _validate_steps: output/instance_ref/instance_uid
|
||||||
|
- `init_db.py` — startup cleanup зависших scenario_runs (>1ч)
|
||||||
|
- `params-render.js` — общий рендер параметров (вынесен из operations.js)
|
||||||
|
|
||||||
|
### Формат шагов (JSONB, без изменений схемы)
|
||||||
|
```json
|
||||||
|
{"service_id": 1, "operation": "create", "params": {}, "output": "d1"}
|
||||||
|
{"service_id": 1, "operation": "modify", "params": {}, "instance_ref": "d1"}
|
||||||
|
```
|
||||||
|
Старый формат (без output/ref) — обратная совместимость через fallback.
|
||||||
|
|
||||||
|
## ❌ Не реализовано (Фаза 4)
|
||||||
|
|
||||||
|
### Модальный редактор сценариев
|
||||||
|
- `scenario-form.js` — нужна полная переделка
|
||||||
|
- `index.html` — разметка модала
|
||||||
|
- Дропдауны сервисов/операций, автоподгрузка параметров
|
||||||
|
- Поля output/instance_ref
|
||||||
|
- [↑][↓] перестановка шагов
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Prompt for Opus — Frontend Scenario Editor UI
|
||||||
|
|
||||||
|
## Что это за проект
|
||||||
|
|
||||||
|
Nubes Autotest — Flask + vanilla JS + PostgreSQL. Автотесты для облачной платформы Nubes.
|
||||||
|
Текущая версия: v1.2.3. Backend-рефакторинг завершён (unified executor, гибкие ссылки).
|
||||||
|
Сейчас нужно доработать **фронтенд редактора сценариев**.
|
||||||
|
|
||||||
|
**ВСЕ файлы фронтенда лежат здесь:**
|
||||||
|
`/home/naeel/nubes/autotest/app-autotest/site/`
|
||||||
|
|
||||||
|
## Файлы фронтенда (читай в этом порядке)
|
||||||
|
|
||||||
|
### 1. Страница и стили
|
||||||
|
`/home/naeel/nubes/autotest/app-autotest/site/templates/index.html`
|
||||||
|
- Три колонки: infra (280px) | svc (240px) | main (flex)
|
||||||
|
- В main: inst-list → params-area → create-btn-area → history-card → scenario-card
|
||||||
|
- Модал: #scenario-modal (уже есть разметка в CSS)
|
||||||
|
- Загрузка JS: utils.js → params-render.js → instances.js → operations.js → history.js → scenario-form.js → scenario-create.js → scenario-edit.js → scenario-delete.js → scenario-list.js → app.js
|
||||||
|
|
||||||
|
### 2. Текущий редактор сценариев (НЕДАВНО ПЕРЕДЕЛАН — изучи ВЕСЬ файл)
|
||||||
|
`/home/naeel/nubes/autotest/app-autotest/site/static/js/scenario-form.js` (~170 строк)
|
||||||
|
- `showScenarioEditor(def)` — открывает модал, загружает /api/services и /api/instances/list в state
|
||||||
|
- `renderEditor()` — рендерит модал: название, шаги, кнопки Сохранить/Отмена
|
||||||
|
- `renderStepRow(idx, step)` — рендерит ОДИН шаг:
|
||||||
|
- Операция: `<select>` с create/delete/modify/suspend/resume/redeploy
|
||||||
|
- Если create: `<select>` сервисов + `<input>` output (имя для ссылок)
|
||||||
|
- Если не-create: `<select>` инстансов (🆕 output'ы предыдущих шагов + ☁ облачные)
|
||||||
|
- Параметры: key=value строки (ручной ввод)
|
||||||
|
- `collectFormSteps()` — собирает данные формы в массив шагов для POST/PUT
|
||||||
|
- `saveScenario()` — валидация + POST/PUT /api/scenario/definitions
|
||||||
|
- `addStep()`, `removeStep()`, `moveStep()`, `addParam()`, `removeParam()`
|
||||||
|
|
||||||
|
### 3. Рендер параметров (общий модуль)
|
||||||
|
`/home/naeel/nubes/autotest/app-autotest/site/static/js/params-render.js` (~80 строк)
|
||||||
|
- `renderParamRow(p, allInst)` — рендер ОДНОГО параметра с учётом типа (select для valueList, refSvcId, boolean; input для текста; map-fixed → renderMapFixedRow)
|
||||||
|
- `renderMapFixedRow(p, dfl)` — вложенные поля для map-fixed параметров
|
||||||
|
- `collectParams(containerSelector)` — сбор параметров из DOM
|
||||||
|
- **Эти функции НЕ используются в scenario-form.js** — там параметры вводятся вручную key=value
|
||||||
|
|
||||||
|
### 4. Список сценариев и кнопки
|
||||||
|
`/home/naeel/nubes/autotest/app-autotest/site/static/js/scenario-list.js` (~90 строк)
|
||||||
|
- `loadScenarios()` — список сценариев + последний запуск (фильтр по версии)
|
||||||
|
- Кнопки: ▶ Запустить | ✏ Редактировать | 📋 Копировать | 🗑 Удалить | + Создать
|
||||||
|
- `runScenario(defId)` — запуск + поллинг
|
||||||
|
|
||||||
|
### 5. Остальные JS (для контекста, глубоко не читай)
|
||||||
|
- `utils.js` — _esc(), validateJson(), relativeTime(), лог-панель
|
||||||
|
- `instances.js` — selectService(), toggleInstance(), renderInstances()
|
||||||
|
- `operations.js` — startCreate(), runOp(), showParams(), executeOp() — здесь showParams использует params-render.js
|
||||||
|
- `history.js` — история тестов
|
||||||
|
- `scenario-create.js` — createScenario(), cloneScenario()
|
||||||
|
- `scenario-edit.js` — editScenario(defId)
|
||||||
|
- `scenario-delete.js` — deleteScenario(defId, name)
|
||||||
|
|
||||||
|
### 6. API (для понимания что можно дёргать)
|
||||||
|
- `GET /api/services` → [{svcId, svc}]
|
||||||
|
- `GET /api/instances/list` → [{instanceUid, displayName, serviceId, svc, explainedStatus}]
|
||||||
|
- `GET /api/operations/{svcId}` → {operations: [{svcOperationId, operation}]}
|
||||||
|
- `GET /api/params/{svcOperationId}` → {params: [{svcOperationCfsParamId, name, dataType, isRequired, defaultValue, valueList, refSvcId, dataDescriptor}]}
|
||||||
|
- `GET/POST /api/scenario/definitions` — список/создать
|
||||||
|
- `GET/PUT/DELETE /api/scenario/definitions/<id>` — один/обновить/удалить
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Текущее состояние редактора
|
||||||
|
|
||||||
|
✅ Работает: модал, дропдауны сервисов/операций, output/ref, кнопки CRUD, сохранение
|
||||||
|
❌ **Параметры — ручной ввод key=value.** Не используется params-render.js. Пользователь должен знать коды параметров (durationMs, failAtStart и т.д.)
|
||||||
|
❌ Нет автоподгрузки параметров при выборе операции
|
||||||
|
❌ Нет валидации параметров в редакторе
|
||||||
|
❌ Параметры в БД хранятся символическими именами (durationMs), резолвятся в numeric ID в runtime
|
||||||
|
|
||||||
|
## Что нужно спроектировать
|
||||||
|
|
||||||
|
**Полноценный редактор параметров в модале сценария.** Чтобы при выборе операции автоматически подгружались параметры из API и показывались с типами, дефолтами, valueList — как в ручном режиме (showParams в operations.js).
|
||||||
|
|
||||||
|
### Конкретные вопросы
|
||||||
|
|
||||||
|
1. **Как встроить params-render.js в редактор сценария?** Сейчас `showScenarioEditor` загружает сервисы. При выборе операции в шаге — нужно: (а) узнать svcOperationId (GET /api/operations/{svcId}), (б) загрузить параметры (GET /api/params/{svcOperationId}), (в) отрендерить их через `renderParamRow`. Где хранить загруженные параметры? В `scenarioEditorState`? В отдельном кеше?
|
||||||
|
|
||||||
|
2. **Как хранить параметры в состоянии шага?** Сейчас `step.params` — массив `[["key","val"],...]`. После загрузки из API там будут десятки параметров с дефолтами. Пользователь меняет некоторые. При сохранении — сохранять только изменённые или все? (Сейчас в БД — только изменённые, остальные берутся из дефолтов при запуске).
|
||||||
|
|
||||||
|
3. **Pre-fill параметров.** При смене операции: авто или по кнопке «Загрузить параметры»? При смене сервиса (create) — сбрасывать параметры?
|
||||||
|
|
||||||
|
4. **Отображение.** Где рендерить параметры в шаге: внутри карточки шага (может быть много) или в отдельной панели справа? Сейчас `operations.js` рендерит параметры в `#params-form` под списком инстансов — это отдельная область.
|
||||||
|
|
||||||
|
5. **Сбор параметров при сохранении.** `collectFormSteps()` сейчас собирает key=value. После перехода на params-render нужно будет использовать `collectParams()` с указанием контейнера шага. Как передать контейнер?
|
||||||
|
|
||||||
|
6. **Map-fixed параметры.** В operations.js они рендерятся с вложенными полями. В сценарном редакторе — нужен такой же рендер? Или упрощённый (JSON-строка)?
|
||||||
|
|
||||||
|
7. **Что делать с существующими сценариями?** У них параметры уже в БД как key=value. При редактировании — показывать только те поля, которые есть в steps? Или подгрузить все из API и смержить?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**ВАЖНО:** Пиши план прямо в чат текстом. Не сохраняй в session memory — я не смогу прочитать. Только в чат.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Вопросы к Опусу — instance dropdown + RUNNING highlight
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
Проект: Nubes Autotest v1.2.5, Flask + vanilla JS + PostgreSQL.
|
||||||
|
Файл: `/home/naeel/nubes/autotest/app-autotest/site/static/js/scenario-form.js`
|
||||||
|
|
||||||
|
Для не-create операций (modify, delete, suspend, resume, redeploy) нужно выбрать инстанс.
|
||||||
|
|
||||||
|
**Что уже сделано (в рабочей копии, не закоммичено):**
|
||||||
|
- Дропдаун показывает ТОЛЬКО 🆕 output'ы предыдущих create-шагов
|
||||||
|
- Кнопка «📥 Из облака» — есть в разметке, функция `toggleCloudInstances(idx)` не написана
|
||||||
|
- Облачные инстансы загружены в `scenarioEditorState.cloudInstances`
|
||||||
|
|
||||||
|
## Вопросы
|
||||||
|
|
||||||
|
### 1. Фильтр облачных инстансов
|
||||||
|
|
||||||
|
Показывать только `displayName.startsWith('autotest-')`. Чужие инстансы — опасно.
|
||||||
|
|
||||||
|
### 2. Реализация кнопки «📥 Из облака»
|
||||||
|
|
||||||
|
При клике:
|
||||||
|
- Отфильтровать `st.cloudInstances`: `autotest-*` + `explainedStatus !== 'deleted'`
|
||||||
|
- Добавить `<option disabled>── облако ──</option>` в основной `<select id="step-{idx}-ref">`
|
||||||
|
- Затем добавить `<option>` для каждого: `☁ displayName — svc (status)`, value = instanceUid
|
||||||
|
- При повторном клике — убрать облачные option'ы (оставить output'ы)
|
||||||
|
|
||||||
|
Фильтр по `service_id` — не нужен, показывать все `autotest-*`.
|
||||||
|
|
||||||
|
### 3. Яркий фон для RUNNING
|
||||||
|
|
||||||
|
Файл: `/home/naeel/nubes/autotest/app-autotest/site/static/js/scenario-list.js`
|
||||||
|
|
||||||
|
В `loadScenarios()` строка:
|
||||||
|
```
|
||||||
|
html+=`<div style="...">Последний: ${icon} ${...} — ${last.status} ...</div>`;
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `last.status === 'RUNNING'` — `background:#fef3c7;padding:2px 4px;border-radius:3px;`.
|
||||||
|
На кнопке `▶` — не надо.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Пиши ответ прямо в чат текстом.
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Prompt for Opus — Code Review v1.2.0
|
||||||
|
|
||||||
|
Проект: Nubes Autotest (Flask + vanilla JS + PostgreSQL)
|
||||||
|
План: `/home/naeel/nubes/autotest/DOCS/opus-plan-2026-07-31.md`
|
||||||
|
Вопросы/ответы: `/home/naeel/nubes/autotest/DOCS/opus-questions-2026-07-31.md`
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
Реализованы Фазы 1-3 + начало Фазы 4. Модальный редактор сценариев НЕ сделан.
|
||||||
|
|
||||||
|
## Изменённые/новые файлы (для ревью)
|
||||||
|
|
||||||
|
### Новые файлы:
|
||||||
|
|
||||||
|
**1. `/home/naeel/nubes/autotest/app-autotest/site/api/utils.py`**
|
||||||
|
- `find_uid(resp)` — поиск UUID по верхнему уровню + вложенным dict
|
||||||
|
- `uid_from_location(loc)` — UUID из Location-заголовка
|
||||||
|
- Заменяет 2 разные реализации из api_test.py + scenario.py
|
||||||
|
|
||||||
|
**2. `/home/naeel/nubes/autotest/app-autotest/site/operations/executor.py`**
|
||||||
|
- `execute_operation(client, service_id, operation, instance_uid, params, svc_op_id=None, display_name=None)`
|
||||||
|
- Делает всё до /run: POST /instances → POST /instanceOperations → send_params_terraform → /run
|
||||||
|
- Ветвится по operation=="create" (свой payload, без svcOperationId)
|
||||||
|
- Возвращает dict {ok, error, failed_step, instance_uid, op_uid, display_name}
|
||||||
|
- НЕ поллит
|
||||||
|
|
||||||
|
**3. `/home/naeel/nubes/autotest/app-autotest/site/operations/poll.py`**
|
||||||
|
- `poll_until_done(client, op_uid, timeout=1800)` → {status, is_successful, error_log, stages, duration, svc}
|
||||||
|
- Общий цикл для async (_finish_op) и sync (run_scenario)
|
||||||
|
|
||||||
|
**4. `/home/naeel/nubes/autotest/app-autotest/site/static/js/params-render.js`**
|
||||||
|
- `renderParamRow(p, allInst)` — рендер параметра
|
||||||
|
- `renderMapFixedRow(p, dfl)` — рендер map-fixed с вложенными полями
|
||||||
|
- `collectParams(containerSelector)` — сбор параметров из DOM
|
||||||
|
- Вынесено из operations.js, используется обоими (ручной + сценарный)
|
||||||
|
|
||||||
|
### Изменённые файлы:
|
||||||
|
|
||||||
|
**5. `/home/naeel/nubes/autotest/app-autotest/site/operations/scenario.py`**
|
||||||
|
- Удалены _find_uid, _uid_from_location
|
||||||
|
- Добавлен import из executor, poll
|
||||||
|
- `_resolve_instance_uid(step, bindings, instance_map)` — приоритет: uid > ref > service_id
|
||||||
|
- `_update_bindings(scenario_run_id, bindings)` — персист в scenario_runs.instance_bindings
|
||||||
|
- `run_scenario` — create/non-create через executor, поллинг через poll_until_done, output→bindings
|
||||||
|
|
||||||
|
**6. `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py`**
|
||||||
|
- Удалены _find_uid, _uid_from_location (импорт из api.utils)
|
||||||
|
- CREATE и non-CREATE флоу заменены на execute_operation
|
||||||
|
- _finish_op заменён на poll_until_done
|
||||||
|
- CMDB delete оставлен как предпроверка (не через executor)
|
||||||
|
|
||||||
|
**7. `/home/naeel/nubes/autotest/app-autotest/site/routes/api_scenario_defs.py`**
|
||||||
|
- `_validate_steps` расширен: output (уникальность, только create), instance_ref (ссылка на output), instance_uid
|
||||||
|
- Старый формат (без новых ключей) проходит валидацию (обратная совместимость)
|
||||||
|
|
||||||
|
**8. `/home/naeel/nubes/autotest/app-autotest/site/db/init_db.py`**
|
||||||
|
- Startup cleanup: `UPDATE scenario_runs SET status='TIMEOUT' WHERE status='RUNNING' AND created_at < NOW() - INTERVAL '1 hour'`
|
||||||
|
|
||||||
|
**9. `/home/naeel/nubes/autotest/app-autotest/site/static/js/operations.js`**
|
||||||
|
- Удалены renderParamRow, renderMapFixedRow, collectParams (теперь в params-render.js)
|
||||||
|
|
||||||
|
**10. `/home/naeel/nubes/autotest/app-autotest/site/templates/index.html`**
|
||||||
|
- Добавлен `<script src="js/params-render.js">` перед instances.js
|
||||||
|
|
||||||
|
**11. `/home/naeel/nubes/autotest/app-autotest/site/app.py`**
|
||||||
|
- VERSION = "1.2.0"
|
||||||
|
|
||||||
|
## Вопросы для ревью
|
||||||
|
|
||||||
|
1. **executor.py**: корректно ли обрабатываются все ошибки? Не потерялся ли tracker_add (в старом коде был ДО params/run, в executor его нет — он вызывается в api_test.py после executor)?
|
||||||
|
2. **scenario.py**: правильно ли резолвится instance_uid? Не сломается ли старый dummy_test без output/instance_ref?
|
||||||
|
3. **api_test.py**: CMDB delete остался как предпроверка — ок ли это?
|
||||||
|
4. **init_db.py**: startup cleanup — не затрёт ли легитимные долгоиграющие сценарии?
|
||||||
|
5. **params-render.js**: `collectParams` теперь с параметром `containerSelector` — не сломает ли это существующие вызовы без аргумента?
|
||||||
|
6. Общая архитектура: все ли зависимости корректны? Нет ли циклических импортов?
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
# План: мок-полигон для интеграционных тестов
|
||||||
|
|
||||||
|
Дата: 2026-07-31. Архитектор: Опус. Утверждено: все 10 решений.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Концепция
|
||||||
|
|
||||||
|
Отдельный Flask-процесс на порту 5001, притворяющийся Nubes API.
|
||||||
|
Data-driven: сервисы описаны в `polygon/services/*.yaml`, эмулятор достраивает
|
||||||
|
недостающие поля по типу. Состояние инстансов/операций — в памяти,
|
||||||
|
`dtFinish` вычисляется лениво. Приложение ходит в мок реальным HTTP через
|
||||||
|
существующий `http_client`.
|
||||||
|
|
||||||
|
Никакой БД, никакого облака, никакого Kubernetes. Только HTTP-ответы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Принятые решения (10/10)
|
||||||
|
|
||||||
|
| Q | Решение | Обоснование |
|
||||||
|
|---|---------|-------------|
|
||||||
|
| Q1 | Отдельный процесс :5001 (A) | `http_client` делает реальные GET/POST/Location — blueprint не проверит |
|
||||||
|
| Q2 | Папка `polygon/services/*.yaml` | Каждый сервис в своём файле, легко добавлять |
|
||||||
|
| Q3 | Мин. поля (id+код+тип), остальное достраивается | 20+ параметров вручную — ад. `default_for(dataType)` |
|
||||||
|
| Q4 | Ленивый dtFinish (A) | Без потоков, детерминированно, `MOCK_OP_DELAY` (по умолчанию 0.1с) |
|
||||||
|
| Q5 | Единая стейт-машина | create→running→suspended→deleted, без кастомизаций |
|
||||||
|
| Q6 | Реальный мерж params | Иначе тест modify→проверить state.params бессмысленен |
|
||||||
|
| Q7 | Статический stateOut из YAML | Для MVP, генерация из параметров — потом |
|
||||||
|
| Q8 | `/_mock/reset` | Без сброса тесты влияют друг на друга |
|
||||||
|
| Q9 | Тесты через `app_client` | Проверяет реальную связку app-autotest ↔ эмулятор |
|
||||||
|
| Q10 | refSvcId игнорируем в MVP | validate-cfs всегда OK, ссылки не проверяются |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Критические точки интеграции
|
||||||
|
|
||||||
|
### 3.1 Location обязателен
|
||||||
|
|
||||||
|
`HttpClient.post` достаёт UUID из заголовка `Location` (последний сегмент, `len >= 32`).
|
||||||
|
Мок ОБЯЗАН отдавать `Location: ./<uuid-36>` на:
|
||||||
|
- `POST /instances` → `Location: ./{instanceUid}`
|
||||||
|
- `POST /instanceOperations` → `Location: ./{instanceOperationUid}`
|
||||||
|
|
||||||
|
Без Location executor не получит instanceUid/opUid.
|
||||||
|
|
||||||
|
### 3.2 Поллинг спит 5с
|
||||||
|
|
||||||
|
`poll_until_done` делает GET, затем `time.sleep(5)` в цикле.
|
||||||
|
При `MOCK_OP_DELAY=0` dtFinish появится на первом же GET — вторая итерация со сном не случится.
|
||||||
|
|
||||||
|
### 3.3 Short-circuit localhost
|
||||||
|
|
||||||
|
`detect_endpoint()` хардкодит dev/test стенды и не читает `NUBES_API_ENDPOINT`.
|
||||||
|
Даже при `NUBES_API_ENDPOINT=http://localhost:5001` он будет долбиться в реальные стенды.
|
||||||
|
|
||||||
|
Решение: проверка `localhost`/`127.0.0.1` в `get_client()` и `get_stand()` (auth.py):
|
||||||
|
|
||||||
|
```
|
||||||
|
если endpoint.startswith("http://localhost") или "http://127.0.0.1":
|
||||||
|
пропустить detect_endpoint()
|
||||||
|
вернуть HttpClient(endpoint, token)
|
||||||
|
stand → "mock"
|
||||||
|
иначе:
|
||||||
|
прежняя логика
|
||||||
|
```
|
||||||
|
|
||||||
|
Ноль новых env-переменных. `NUBES_API_ENDPOINT` уже есть в конфиге.
|
||||||
|
|
||||||
|
### 3.4 validate-cfs = пустое тело
|
||||||
|
|
||||||
|
`send_params_terraform` считает успехом пустой/не-JSON ответ.
|
||||||
|
Мок отдаёт `200` с пустым телом.
|
||||||
|
|
||||||
|
### 3.5 state.params по коду, cfsParams по числовому id
|
||||||
|
|
||||||
|
`get_params_with_current_values` мержит `state.params[код]` с шаблоном из
|
||||||
|
`GET /instanceOperations/default/{opId}`.
|
||||||
|
YAML должен связывать числовой `svcOperationCfsParamId` ↔ код параметра.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Структура файлов
|
||||||
|
|
||||||
|
```
|
||||||
|
app-autotest/
|
||||||
|
├── site/
|
||||||
|
│ ├── api/
|
||||||
|
│ │ └── auth.py # ИЗМЕНИТЬ: +short-circuit localhost
|
||||||
|
│ └── ...
|
||||||
|
├── polygon/ # НОВАЯ папка
|
||||||
|
│ ├── server.py # Flask-приложение эмулятора
|
||||||
|
│ ├── state.py # MockState (в памяти)
|
||||||
|
│ ├── config_loader.py # загрузка YAML + достройка defaults
|
||||||
|
│ ├── defaults.py # default_for(dataType)
|
||||||
|
│ └── services/
|
||||||
|
│ ├── dummy.yaml # Болванка (реальные ID из HAR)
|
||||||
|
│ └── postgresql.yaml # PostgreSQL (stateOut: users, databases)
|
||||||
|
└── tests/
|
||||||
|
├── conftest.py # ИЗМЕНИТЬ: +фикстура поднятия мока
|
||||||
|
└── test_mock_integration.py # НОВЫЙ: интеграционные тесты
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. План реализации (3 фазы, 12 шагов)
|
||||||
|
|
||||||
|
### Фаза 1 — MVP (create + поллинг)
|
||||||
|
|
||||||
|
**Шаг 1. `polygon/defaults.py`**
|
||||||
|
Функция `default_for(dataType)`: `int→"0"`, `bool→"false"`, `array→"[]"`,
|
||||||
|
`map/json→"{}"`, иначе `""`. Зеркалит `normalize_value` из terraform.py.
|
||||||
|
|
||||||
|
**Шаг 2. `polygon/config_loader.py`**
|
||||||
|
- Грузит все `polygon/services/*.yaml`
|
||||||
|
- Для каждого cfsParam достраивает недостающие поля
|
||||||
|
(`defaultValue`, `valueList`, `isRequired`, `refSvcId`, `dataDescriptor`)
|
||||||
|
через `default_for()`
|
||||||
|
- Возвращает dict: `{service_id: {svc, operations, cfsParams, stateParams, stateOut}}`
|
||||||
|
|
||||||
|
**Шаг 3. `polygon/state.py` — MockState**
|
||||||
|
```
|
||||||
|
class MockState:
|
||||||
|
instances: dict[uid] → {serviceId, displayName, status, params, ...}
|
||||||
|
operations: dict[opUid] → {instanceUid, svcOperationId, operation, dtRunStart, params, ...}
|
||||||
|
|
||||||
|
create_instance(service_id, display_name) → instanceUid
|
||||||
|
create_operation(instanceUid, svcOperationId, operation) → opUid
|
||||||
|
set_param(opUid, paramId, value)
|
||||||
|
run(opUid) — записывает dtRunStart
|
||||||
|
get_operation(opUid) → {dtFinish, isSuccessful, ...} (ленивый dtFinish)
|
||||||
|
apply_effect(opUid) — modify→мерж params, delete→удаление инстанса, suspend/resume→статус
|
||||||
|
reset()
|
||||||
|
```
|
||||||
|
|
||||||
|
Ленивый dtFinish: при GET `/instanceOperations/{uid}` сравнивает
|
||||||
|
`now - dtRunStart >= MOCK_OP_DELAY`. Если да — выставляет `dtFinish=now`,
|
||||||
|
`isSuccessful=True` и вызывает `apply_effect`.
|
||||||
|
|
||||||
|
**Шаг 4. `polygon/server.py`**
|
||||||
|
Flask-приложение, префикс `/api/v1/svc`. На этом шаге — минимальный набор:
|
||||||
|
- `POST /instances` — создаёт инстанс, возвращает 201 + `Location: ./{uid}`
|
||||||
|
- `POST /instanceOperations` — создаёт операцию, `Location: ./{opUid}`
|
||||||
|
- `POST /instanceOperationCfsParams` — устанавливает параметр
|
||||||
|
- `POST /instanceOperations/{uid}/run` — запускает операцию
|
||||||
|
- `GET /instanceOperations/{uid}?fields=...` — статус операции (ленивый dtFinish)
|
||||||
|
- `GET /instanceOperations/{uid}/validate-cfs` — 200 OK, пустое тело
|
||||||
|
|
||||||
|
**Шаг 5. `polygon/services/dummy.yaml`**
|
||||||
|
Реальные ID из HAR (распарсены Опусом):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
1: # serviceId
|
||||||
|
svc: "Болванка"
|
||||||
|
svcShort: "dummy"
|
||||||
|
svcExtendedName: "Болванка"
|
||||||
|
operations:
|
||||||
|
- {svcOperationId: 18, operation: create, isCreate: true}
|
||||||
|
- {svcOperationId: 92, operation: modify}
|
||||||
|
- {svcOperationId: 71, operation: delete}
|
||||||
|
- {svcOperationId: 93, operation: suspend}
|
||||||
|
- {svcOperationId: 94, operation: resume}
|
||||||
|
- {svcOperationId: 240, operation: redeploy}
|
||||||
|
cfsParams:
|
||||||
|
- {svcOperationCfsParamId: 242, svcOperationCfsParam: "resourceRealm", dataType: "string", valueList: ["dummy"], isRequired: true, defaultValue: "dummy"}
|
||||||
|
- {svcOperationCfsParamId: 198, svcOperationCfsParam: "durationMs", dataType: "integer >= 0", defaultValue: "0"}
|
||||||
|
- {svcOperationCfsParamId: 199, svcOperationCfsParam: "param199", dataType: "boolean", defaultValue: "false"}
|
||||||
|
- {svcOperationCfsParamId: 200, svcOperationCfsParam: "param200", dataType: "boolean", defaultValue: "false"}
|
||||||
|
- {svcOperationCfsParamId: 201, svcOperationCfsParam: "param201", dataType: "integer", defaultValue: "1"}
|
||||||
|
- {svcOperationCfsParamId: 286, svcOperationCfsParam: "param286", dataType: "string"}
|
||||||
|
- {svcOperationCfsParamId: 321, svcOperationCfsParam: "param321", dataType: "map", dataDescriptor: {subparam1: {dataType: "string"}, secret: {dataType: "string"}, subparam2: {dataType: "string"}}}
|
||||||
|
- {svcOperationCfsParamId: 322, svcOperationCfsParam: "param322", dataType: "string"}
|
||||||
|
- {svcOperationCfsParamId: 396, svcOperationCfsParam: "param396", dataType: "string"}
|
||||||
|
- {svcOperationCfsParamId: 647, svcOperationCfsParam: "param647", dataType: "map", dataDescriptor: {bol1: {dataType: "boolean"}, minStr1: {dataType: "string"}, param1: {dataType: "string"}, param2: {dataType: "string"}, param3: {dataType: "string"}}}
|
||||||
|
- {svcOperationCfsParamId: 654, svcOperationCfsParam: "param654", dataType: "array", dataDescriptor: {bol1: {dataType: "boolean"}, minStr1: {dataType: "string"}, param1: {dataType: "string"}, param2: {dataType: "string"}, param3: {dataType: "string"}}}
|
||||||
|
- {svcOperationCfsParamId: 863, svcOperationCfsParam: "param863", dataType: "array"}
|
||||||
|
stateParams:
|
||||||
|
resourceRealm: "dummy"
|
||||||
|
durationMs: "0"
|
||||||
|
param199: "false"
|
||||||
|
param200: "false"
|
||||||
|
param201: "1"
|
||||||
|
stateOut: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 6. Интеграция в `auth.py`**
|
||||||
|
Добавить short-circuit в `get_client()` и `get_stand()`:
|
||||||
|
```python
|
||||||
|
def _is_localhost(endpoint):
|
||||||
|
return (endpoint or "").startswith(("http://localhost", "http://127.0.0.1"))
|
||||||
|
|
||||||
|
def get_client():
|
||||||
|
token = get_token()
|
||||||
|
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
||||||
|
if not _is_localhost(endpoint):
|
||||||
|
endpoint = detect_endpoint(token) or endpoint
|
||||||
|
return HttpClient(endpoint, token)
|
||||||
|
|
||||||
|
def get_stand():
|
||||||
|
token = get_token()
|
||||||
|
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
||||||
|
if _is_localhost(endpoint):
|
||||||
|
return "mock"
|
||||||
|
endpoint = detect_endpoint(token) or endpoint
|
||||||
|
return stand_name(endpoint)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Фаза 2 — полный CRUD + сервисы
|
||||||
|
|
||||||
|
**Шаг 7. GET-эндпоинты**
|
||||||
|
- `GET /instances?pageSize=N&page=P` — список с пагинацией (pageSize≤200, стоп по `len(batch)<pageSize`)
|
||||||
|
- `GET /instances/{uid}` — `{instance: {instanceUid, displayName, serviceId, svc, explainedStatus, state: {params: {...}, out: {...}}}}`
|
||||||
|
|
||||||
|
**Шаг 8. GET-эндпоинты (сервисы)**
|
||||||
|
- `GET /services` → `{results: [{svcId, svc, svcExtendedName}]}`
|
||||||
|
- `GET /services/{id}` → `{svc: {svc, svcShort, operations: [...]}}`
|
||||||
|
- `GET /instanceOperations/default/{id}` → `{svcOperation: {cfsParams: [...]}}`
|
||||||
|
|
||||||
|
**Шаг 9. `apply_effect` в MockState**
|
||||||
|
- modify → мерж params в `state.params` инстанса
|
||||||
|
- delete → удаление инстанса из `state.instances`
|
||||||
|
- suspend → статус `suspended`
|
||||||
|
- resume → статус `running`
|
||||||
|
- redeploy → статус `running`
|
||||||
|
|
||||||
|
**Шаг 10. `/_mock/reset` + postgresql.yaml**
|
||||||
|
- `POST /_mock/reset` — сброс всего состояния
|
||||||
|
- `polygon/services/postgresql.yaml`:
|
||||||
|
```yaml
|
||||||
|
21: # serviceId (подставить реальный)
|
||||||
|
svc: "postgresql"
|
||||||
|
svcShort: "pg"
|
||||||
|
operations:
|
||||||
|
- {svcOperationId: 300, operation: create}
|
||||||
|
- {svcOperationId: 301, operation: modify}
|
||||||
|
- {svcOperationId: 302, operation: delete}
|
||||||
|
- {svcOperationId: 303, operation: suspend}
|
||||||
|
- {svcOperationId: 304, operation: resume}
|
||||||
|
cfsParams:
|
||||||
|
- {svcOperationCfsParamId: 401, svcOperationCfsParam: "dbName", dataType: "string", defaultValue: "mydb"}
|
||||||
|
- {svcOperationCfsParamId: 402, svcOperationCfsParam: "dbUser", dataType: "string", defaultValue: "pgadmin"}
|
||||||
|
- {svcOperationCfsParamId: 403, svcOperationCfsParam: "dbPassword", dataType: "string", defaultValue: "***"}
|
||||||
|
- {svcOperationCfsParamId: 404, svcOperationCfsParam: "version", dataType: "string", valueList: ["14", "15", "16"], defaultValue: "15"}
|
||||||
|
stateParams:
|
||||||
|
dbName: "mydb"
|
||||||
|
dbUser: "pgadmin"
|
||||||
|
version: "15"
|
||||||
|
stateOut:
|
||||||
|
users: {pgadmin: {}, appuser: {}}
|
||||||
|
databases: {mydb: {}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Фаза 3 — тесты
|
||||||
|
|
||||||
|
**Шаг 11. `tests/conftest.py`**
|
||||||
|
Фикстура поднятия мок-процесса + автосброс через `/_mock/reset`:
|
||||||
|
```python
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def mock_server():
|
||||||
|
# Запустить polygon/server.py на порту 5001
|
||||||
|
# Дождаться готовности
|
||||||
|
# yield
|
||||||
|
# Остановить процесс
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def reset_mock(mock_server):
|
||||||
|
requests.post("http://localhost:5001/_mock/reset")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 12. `tests/test_mock_integration.py`**
|
||||||
|
5 сценариев через `app_client`:
|
||||||
|
|
||||||
|
1. **create Болванку** — `instanceUid` не пуст, `GET /instances/{uid}` → статус `running`
|
||||||
|
2. **modify параметр** — `state.params` показывает новое значение
|
||||||
|
3. **delete** — инстанс исчез из `GET /instances`
|
||||||
|
4. **Сценарий create→modify→delete** — `POST /api/scenario/run` проходит целиком
|
||||||
|
5. **PG create** — `state.out.users` и `state.out.databases` заполнены
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Формат services.yaml (спецификация)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
<serviceId>:
|
||||||
|
svc: "имя_сервиса" # обязательное
|
||||||
|
svcShort: "короткое_имя" # опциональное
|
||||||
|
svcExtendedName: "полное" # опциональное
|
||||||
|
operations: # обязательное
|
||||||
|
- svcOperationId: <int> # обязательное
|
||||||
|
operation: <str> # create|modify|delete|suspend|resume|redeploy
|
||||||
|
isCreate: <bool> # опциональное (true для create)
|
||||||
|
cfsParams: # опциональное (мин. поля обязательны)
|
||||||
|
- svcOperationCfsParamId: <int> # обязательное
|
||||||
|
svcOperationCfsParam: <str> # обязательное (код параметра)
|
||||||
|
dataType: <str> # обязательное
|
||||||
|
defaultValue: <str> # опц. (достраивается по типу)
|
||||||
|
isRequired: <bool> # опц. (достраивается)
|
||||||
|
valueList: [<str>, ...] # опц.
|
||||||
|
refSvcId: <int> # опц.
|
||||||
|
dataDescriptor: {<key>: {...}} # опц. (для map-параметров)
|
||||||
|
stateParams: # опциональное (значения после create)
|
||||||
|
<код>: <значение>
|
||||||
|
stateOut: # опциональное (доп. данные)
|
||||||
|
users: {<name>: {}}
|
||||||
|
databases: {<name>: {}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила достройки (`default_for`):
|
||||||
|
- `defaultValue` отсутствует → `"0"` для integer, `"false"` для boolean, `"[]"` для array, `"{}"` для map/json, `""` для string
|
||||||
|
- `isRequired` отсутствует → `false`
|
||||||
|
- `valueList` отсутствует → `null` (не select, а input)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Машина состояний
|
||||||
|
|
||||||
|
```
|
||||||
|
create → running
|
||||||
|
suspend → suspended
|
||||||
|
resume → running
|
||||||
|
modify → running (после мержа params)
|
||||||
|
delete → УДАЛЁН (исчезает из GET /instances)
|
||||||
|
redeploy→ running
|
||||||
|
```
|
||||||
|
|
||||||
|
Статусы: `creating` (пока dtFinish не появился), `running`, `suspended`, `deleted`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Зависимости и окружение
|
||||||
|
|
||||||
|
- **Зависимости**: PyYAML (для config_loader), Flask (уже есть)
|
||||||
|
- **Env-переменные**:
|
||||||
|
- `MOCK_OP_DELAY` — задержка операции в секундах (по умолчанию 0.1)
|
||||||
|
- `NUBES_API_ENDPOINT` — `http://localhost:5001/api/v1/svc` (уже есть)
|
||||||
|
- **Порт**: 5001 (не конфликтует с основным приложением на 5000/8000)
|
||||||
|
- **Память**: всё в `MockState`, без БД, без файлов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Проверка (Verification)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Запуск полигона
|
||||||
|
cd app-autotest && NUBES_API_ENDPOINT=http://localhost:5001/api/v1/svc \
|
||||||
|
python polygon/server.py
|
||||||
|
|
||||||
|
# 2. Дымовой тест
|
||||||
|
curl http://localhost:5001/api/v1/svc/instances?pageSize=1
|
||||||
|
# → {"results": []}
|
||||||
|
|
||||||
|
# 3. Интеграционные тесты
|
||||||
|
pytest tests/test_mock_integration.py -v
|
||||||
|
# Все 5 зелёные
|
||||||
|
|
||||||
|
# 4. Полный прогон (старые тесты не сломаны)
|
||||||
|
pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Что НЕ делаем
|
||||||
|
|
||||||
|
- ❌ Реальное выполнение операций (не Terraform, не Ansible)
|
||||||
|
- ❌ Валидация параметров (всегда validate-cfs = OK)
|
||||||
|
- ❌ База данных
|
||||||
|
- ❌ refSvcId-резолв в MVP
|
||||||
|
- ❌ Многопоточность
|
||||||
|
- ❌ Деплой полигона (только локально/CI)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# 2026-07-31 — Сессия (v1.1.57 → ...)
|
# 2026-07-31 — Сессия (v1.1.57 → v1.2.1)
|
||||||
|
|
||||||
## Контекст
|
## Контекст
|
||||||
Обсуждение архитектуры: унификация ручного и сценарного режимов, гибкие ссылки на инстансы, новый UI редактора сценариев.
|
Обсуждение архитектуры: унификация ручного и сценарного режимов, гибкие ссылки на инстансы, новый UI редактора сценариев.
|
||||||
@@ -69,3 +69,737 @@
|
|||||||
- E17. lock_check оставить
|
- E17. lock_check оставить
|
||||||
- E18. _op_results не в scope
|
- E18. _op_results не в scope
|
||||||
- E19. Порядок: executor → формат → api_test/scenario → UI
|
- E19. Порядок: executor → формат → api_test/scenario → UI
|
||||||
|
|
||||||
|
### Финальный план (Опус, утверждён)
|
||||||
|
|
||||||
|
Сохранён в `DOCS/opus-plan-2026-07-31.md`. Ветка: `opus-architecture-2026-07-31`.
|
||||||
|
|
||||||
|
**4 фазы, 13 шагов:**
|
||||||
|
|
||||||
|
**Фаза 1 — Общие модули:**
|
||||||
|
- `api/utils.py` (NEW) — find_uid(), uid_from_location()
|
||||||
|
- `operations/poll.py` (NEW) — poll_until_done()
|
||||||
|
- `operations/executor.py` (NEW) — execute_operation() до /run, без поллинга
|
||||||
|
|
||||||
|
**Фаза 2 — Формат шагов:**
|
||||||
|
- `routes/api_scenario_defs.py` — _validate_steps с output/instance_ref/instance_uid
|
||||||
|
- `operations/scenario.py` — резолвинг instance_uid > instance_ref > service_id
|
||||||
|
|
||||||
|
**Фаза 3 — Миграция вызывающих:**
|
||||||
|
- `routes/api_test.py` — CMDB delete early return, остальное через executor
|
||||||
|
- `operations/scenario.py` — через executor + poll_until_done
|
||||||
|
- `db/init_db.py` — startup cleanup зависших scenario_runs
|
||||||
|
|
||||||
|
**Фаза 4 — UI редактора:**
|
||||||
|
- `static/js/params-render.js` (NEW) — общий рендер параметров
|
||||||
|
- `static/js/operations.js` — использовать params-render.js
|
||||||
|
- `static/js/scenario-form.js` — модальный редактор
|
||||||
|
- `templates/index.html` — разметка модала
|
||||||
|
|
||||||
|
### CMDB delete
|
||||||
|
Жёсткое удаление через `DELETE cmdb-api.deck.nubes.ru/instances/{uid}` (без авторизации).
|
||||||
|
Нужно для недосозданных инстансов (not created). Остаётся в api_test.py, не в executor.
|
||||||
|
Добавлено после переписки с Георгием Родионовым 29.07.2026.
|
||||||
|
|
||||||
|
## Реализация (v1.2.0 — v1.2.1)
|
||||||
|
|
||||||
|
### v1.2.0 — Unified executor + flexible refs
|
||||||
|
**Новые файлы:** api/utils.py, operations/poll.py, operations/executor.py, static/js/params-render.js
|
||||||
|
**Изменено:** api_test.py (через executor + poll), scenario.py (output/ref/bindings), api_scenario_defs.py (_validate_steps), init_db.py (cleanup), operations.js (→ params-render), index.html (load order)
|
||||||
|
- Дублирование CREATE-флоу устранено: ручной и сценарный → один executor
|
||||||
|
- instance_uid > instance_ref > service_id (гибкие ссылки)
|
||||||
|
- Общий поллинг poll_until_done()
|
||||||
|
- +254 / −277 строк (меньше кода)
|
||||||
|
|
||||||
|
### v1.2.1 — Opus review fixes
|
||||||
|
**Критическое:** tracker_add внутрь executor (защита от сирот, A3)
|
||||||
|
**Исправлено:** labelCls в renderMapFixedRow, descr с контекстом, порядок скриптов
|
||||||
|
**5 файлов:** executor.py, api_test.py, scenario.py, params-render.js, app.py
|
||||||
|
|
||||||
|
### Проверка в поде (v1.2.1)
|
||||||
|
- Все 4 новых файла на месте
|
||||||
|
- 11 JS → 200, порядок правильный
|
||||||
|
- Schema OK, seed OK, API отвечает
|
||||||
|
|
||||||
|
## Что дальше
|
||||||
|
|
||||||
|
**Фаза 4 — модальный редактор сценариев (✅ v1.2.2-v1.2.4):**
|
||||||
|
- ✅ Модал с дропдаунами сервисов/операций
|
||||||
|
- ✅ output/instance_ref с облачными инстансами
|
||||||
|
- ✅ Кнопки CRUD крупнее, справка «📖 Как заполнять»
|
||||||
|
|
||||||
|
### v1.2.16 — instance_meta JSONB
|
||||||
|
После каждого прогона сохраняется полная информация об инстансе (GET /instances/{uid}).
|
||||||
|
Все поля кроме instanceUid/displayName/svc/serviceId/explainedStatus.
|
||||||
|
|
||||||
|
## Идеи на будущее (НЕ ДЕЛАТЬ, обдумать)
|
||||||
|
|
||||||
|
**Context snapshot:** сохранять снапшот ВСЕХ инстансов пользователя на момент запуска
|
||||||
|
(instanceUid, displayName, serviceId, svc, explainedStatus, specification).
|
||||||
|
При анализе FAIL — видеть контекст: «было 3 running Болванки, возможно конфликт ресурсов».
|
||||||
|
Хранить в `runs.context_snapshot JSONB` и `scenario_runs.context_snapshot JSONB`.
|
||||||
|
Данные обезличенные, не гигабайты. Отложено до реальной необходимости.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Аудит безопасности GPT-5.3-Codex (2026-07-31)
|
||||||
|
|
||||||
|
Проведён полный code review 29 файлов (~6000 строк). Найдено 11 проблем.
|
||||||
|
Результаты зафиксированы в DOCS/ARCHITECTURE.md (раздел 8).
|
||||||
|
|
||||||
|
### КРИТИЧЕСКИЕ (исправлены)
|
||||||
|
|
||||||
|
1. **XSS через params в scenario-list.js:119** — k/v параметров в innerHTML без `_esc`.
|
||||||
|
Stored XSS через БД сценариев. → v1.2.19
|
||||||
|
|
||||||
|
2. **JS injection в onclick** (scenario-list.js:126-128) — `def.name` в `'...'` без JS-escape.
|
||||||
|
`_esc` не экранирует `'` → разрыв строки. → v1.2.19
|
||||||
|
|
||||||
|
3. **Гонка `_op_results`** (api_test.py:264-280) — dict без lock, читается/пишется/чистится
|
||||||
|
из нескольких потоков. → v1.2.20: `threading.Lock()` + `pop(k, None)`
|
||||||
|
|
||||||
|
4. **Неатомарный lock сценариев** (scenario_defs.py + api_scenario_run.py) —
|
||||||
|
`lock_check` (SELECT) и `INSERT RUNNING` разделены. → v1.2.20: `pg_try_advisory_lock`
|
||||||
|
|
||||||
|
### СРЕДНИЕ (исправлены)
|
||||||
|
|
||||||
|
5. **Lost update трекера** (tracker.py) — `_locked_read` + `_locked_write` в разных lock.
|
||||||
|
→ v1.2.19: `_atomic_update()` под одним lock
|
||||||
|
|
||||||
|
6. **Зависание UI поллинга** (scenario-list.js:211) — пустой catch, `busy` не сбрасывается.
|
||||||
|
→ v1.2.19: счётчик ошибок + `stopScenarioPoll` + `busy=false`
|
||||||
|
|
||||||
|
7. **`has_target` не проверяется** (api_scenario_defs.py:58) — вычисляется и игнорируется.
|
||||||
|
→ v1.2.19: явная проверка
|
||||||
|
|
||||||
|
8. **`_ensure_schema` silent** (pool.py:64) — `except Exception: pass`.
|
||||||
|
→ v1.2.20: `traceback.print_exc()`
|
||||||
|
|
||||||
|
### ПОТЕНЦИАЛЬНЫЕ (исправлены)
|
||||||
|
|
||||||
|
9. **Stale async в редакторе** (scenario-form.js) — `loadStepParams` после `renderEditor`
|
||||||
|
может перезаписать новый DOM. → v1.2.20: `_renderGen` generation token
|
||||||
|
|
||||||
|
10. **validate-cfs хрупкий** (terraform.py:250-254) — фильтрация по тексту исключения.
|
||||||
|
→ v1.2.20: явный `except json.JSONDecodeError`
|
||||||
|
|
||||||
|
### НЕ ИСПРАВЛЕНО (архитектурное ограничение)
|
||||||
|
|
||||||
|
11. **In-memory `_op_results` на воркер** — не shared между gunicorn-воркерами.
|
||||||
|
Статус иногда читается из API fallback. Решение: Redis/БД для статусов.
|
||||||
|
Отложено — низкая вероятность проблемы на практике (2 воркера, stickiness).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Повторный аудит Codex (2026-07-31, вторая итерация)
|
||||||
|
|
||||||
|
Codex проверил исправления и нашёл **критические ошибки в моих же фиксах**:
|
||||||
|
|
||||||
|
### ОШИБКА AI #1: Advisory lock сломан (v1.2.20)
|
||||||
|
|
||||||
|
**Что я сделал:** `lock_check()` брал `pg_try_advisory_lock` на соединении `conn1`,
|
||||||
|
возвращал `True`, и `conn1` уходил обратно в пул. `unlock_scenario()` вызывал
|
||||||
|
`get_conn()` → получал `conn2` (другое соединение!) → unlock на `conn2` не снимал
|
||||||
|
lock с `conn1`. Плюс ранние `return` в `api_scenario_run.py` после успешного
|
||||||
|
`lock_check` вообще не вызывали unlock.
|
||||||
|
|
||||||
|
**Почему ошибся:** не учёл что PostgreSQL advisory lock привязан к сессии (соединению),
|
||||||
|
а соединения возвращаются в пул. Передача соединения между `lock_check` и потоком
|
||||||
|
сценария требовала бы сложной оркестрации.
|
||||||
|
|
||||||
|
**Как исправлено (v1.2.21):** заменён на **partial unique index** на уровне БД:
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX idx_one_running
|
||||||
|
ON scenario_runs (client_id, stand) WHERE status = 'RUNNING';
|
||||||
|
```
|
||||||
|
Теперь `INSERT INTO scenario_runs ... status='RUNNING'` сам становится атомарной
|
||||||
|
проверкой — вторая вставка получает unique violation → 409 без гонок.
|
||||||
|
`lock_check` возвращён к простому SELECT (быстрая предпроверка для красивого 409).
|
||||||
|
`unlock_scenario` удалён полностью. `try/finally` из `run_scenario` убран.
|
||||||
|
|
||||||
|
### ОШИБКА AI #2: escName не экранирует `"` (v1.2.19)
|
||||||
|
|
||||||
|
**Что я сделал:** `def.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'")` —
|
||||||
|
экранировал `\` и `'` для JS-строки, но забыл `"` для HTML-атрибута `onclick="..."`.
|
||||||
|
|
||||||
|
**Почему ошибся:** фокусировался только на JS-контексте (строка в `'...'`),
|
||||||
|
не учёл что она внутри HTML-атрибута в `"..."`.
|
||||||
|
|
||||||
|
**Как исправлено (v1.2.21):** добавлено `.replace(/"/g,'"')` в `escName`.
|
||||||
|
|
||||||
|
### НОВАЯ находка Codex: instances.js:78
|
||||||
|
|
||||||
|
`instanceUid` в `onclick="toggleInstance('${i.instanceUid}')"` — теоретически уязвим,
|
||||||
|
но на практике UUID всегда `[a-f0-9-]+` → безопасен. Отмечен как низкий риск,
|
||||||
|
исправление не требуется.
|
||||||
|
|
||||||
|
### ИТОГ
|
||||||
|
|
||||||
|
| # | Слой | Статус |
|
||||||
|
|---|------|--------|
|
||||||
|
| Advisory lock | Python | ❌ СЛОМАН → ✅ partial unique index |
|
||||||
|
| escName `"` | JS | ❌ Неполный → ✅ добавлен `"` |
|
||||||
|
| instances.js onclick | JS | ⚠️ Низкий риск, UUID безопасен |
|
||||||
|
| `lock_check` fallback | Python | ✅ `True`→`False` при ошибке БД |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Третий аудит Codex + трёхсостояночный lock_check (v1.2.22-v1.2.23)
|
||||||
|
|
||||||
|
Codex проверил v1.2.21 и нашёл 3 проблемы. Две исправлены, одну — обсудили и
|
||||||
|
пришли к правильному решению:
|
||||||
|
|
||||||
|
### Исправлено
|
||||||
|
|
||||||
|
1. **UniqueViolation → 500 (не 409)** — `api_scenario_run.py:78`.
|
||||||
|
INSERT ловился общим `except` → 500. Теперь: `e.pgcode == '23505'` → 409. (v1.2.22)
|
||||||
|
|
||||||
|
2. **escName без `&`** — `scenario-list.js:128`.
|
||||||
|
`'` декодируется браузером в `'` до JS → разрыв строки.
|
||||||
|
Добавлен `.replace(/&/g,'&')` ПЕРВЫМ шагом. (v1.2.22)
|
||||||
|
|
||||||
|
### Обсуждено и исправлено правильно
|
||||||
|
|
||||||
|
3. **`lock_check` fallback — трёхсостояночный подход** (v1.2.23):
|
||||||
|
|
||||||
|
Исходно Codex предложил `True`→`False` при no-db. AI слепо сделал.
|
||||||
|
Пользователь возразил: `False` ломает запуск при деградации БД.
|
||||||
|
|
||||||
|
Codex согласился и предложил трёхсостояночный возврат:
|
||||||
|
- `True` — можно запускать (нет RUNNING)
|
||||||
|
- `False` — нельзя (есть RUNNING) → 409
|
||||||
|
- `None` — БД недоступна → 503
|
||||||
|
|
||||||
|
`api_scenario_run.py` обрабатывает `None` как 503 DB unavailable.
|
||||||
|
|
||||||
|
### Созданы тесты (Codex, только сохранены, не запущены)
|
||||||
|
|
||||||
|
`tests/` — 4 файла, покрывают критические фиксы:
|
||||||
|
|
||||||
|
| Файл | Что тестирует |
|
||||||
|
|------|---------------|
|
||||||
|
| `conftest.py` | Flask test client + sys.path |
|
||||||
|
| `test_api_scenario_run.py` | 503 при `lock_check=None`, 409 при `False`, 409 при `UniqueViolation(pgcode=23505)` |
|
||||||
|
| `test_db_scenario_defs.py` | `lock_check → None` при no-db и ошибке БД |
|
||||||
|
| `test_static_regressions.py` | Статическая проверка `idx_one_running` в init_db.py и цепочки `escName` в scenario-list.js |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## План: эмулятор Nubes API для интеграционных тестов
|
||||||
|
|
||||||
|
### Зачем
|
||||||
|
Реальные тесты медленные (поллинг до 30 минут) и жрут ресурсы облака.
|
||||||
|
Эмулятор даст: быстрые тесты (< 1 сек), детерминизм, краевые случаи, CI/CD.
|
||||||
|
|
||||||
|
### Архитектура
|
||||||
|
```
|
||||||
|
site/
|
||||||
|
├── app.py # основное приложение
|
||||||
|
└── mock/
|
||||||
|
└── nubes_mock.py # эмулятор API Nubes (Flask, порт 5001)
|
||||||
|
```
|
||||||
|
|
||||||
|
В `app.py` — переключение по `NUBES_MOCK=1` → эндпоинт `http://localhost:5001/api/v1/svc`.
|
||||||
|
|
||||||
|
### Эндпоинты для эмуляции (Болванка, сервис 1)
|
||||||
|
|
||||||
|
| Метод | Путь | Ответ |
|
||||||
|
|-------|------|-------|
|
||||||
|
| POST | `/instances` | 201 + `{instanceUid}` |
|
||||||
|
| GET | `/instances?pageSize=200` | `{results: [...]}` |
|
||||||
|
| GET | `/instances/{uid}` | `{instance: {state: {params: {...}}}}` |
|
||||||
|
| GET | `/services` | `{results: [{svcId: 1, svc: "dummy"}]}` |
|
||||||
|
| GET | `/services/1` | `{svc: {operations: [...]}}` |
|
||||||
|
| POST | `/instanceOperations` | `{instanceOperationUid}` |
|
||||||
|
| POST | `/instanceOperationCfsParams` | `{}` |
|
||||||
|
| POST | `/instanceOperations/{uid}/run` | `{}` |
|
||||||
|
| GET | `/instanceOperations/{uid}?fields=...` | `{instanceOperation: {dtFinish, isSuccessful, ...}}` |
|
||||||
|
| GET | `/instanceOperations/default/{id}` | `{svcOperation: {cfsParams: [...]}}` |
|
||||||
|
| GET | `/instanceOperations/{uid}/validate-cfs` | `{}` (200 OK) |
|
||||||
|
|
||||||
|
### Что сложнее
|
||||||
|
|
||||||
|
- `cfsParams` — у каждого сервиса своя структура, придётся хардкодить под Болванку
|
||||||
|
- `refSvcId` — ссылки на другие сервисы (External IP и т.д.) — отложить
|
||||||
|
- `stages` — этапы выполнения (plan→apply→...) — отдавать фейковые
|
||||||
|
|
||||||
|
### Порядок создания
|
||||||
|
|
||||||
|
1. `mock/nubes_mock.py` — Flask-заглушка (~150 строк)
|
||||||
|
2. Интеграционный тест: сценарий `create→modify→delete` через `app_client`
|
||||||
|
3. `NUBES_MOCK=1` в `app.py` для переключения эндпоинта
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектура мок-полигона (Опус, 2026-07-31)
|
||||||
|
|
||||||
|
### Принятые решения (10/10)
|
||||||
|
|
||||||
|
| Q | Решение | Обоснование |
|
||||||
|
|---|---------|-------------|
|
||||||
|
| Q1 | Отдельный процесс :5001 (A) | `http_client` делает реальные GET/POST — blueprint не проверит |
|
||||||
|
| Q2 | Папка `polygon/services/*.yaml` | Каждый сервис в своём файле |
|
||||||
|
| Q3 | Мин. поля (id+код+тип), остальное достраивается | 20+ параметров вручную — ад |
|
||||||
|
| Q4 | Ленивый dtFinish (A) | Без потоков, детерминированно, `MOCK_OP_DELAY=0.1` |
|
||||||
|
| Q5 | Единая стейт-машина | create→running→suspended→deleted |
|
||||||
|
| Q6 | Реальный мерж params | Иначе тест modify→проверить state.params бессмысленен |
|
||||||
|
| Q7 | Статический stateOut из YAML | Для MVP, генерация потом |
|
||||||
|
| Q8 | `/_mock/reset` | Без сброса тесты влияют друг на друга |
|
||||||
|
| Q9 | Тесты через `app_client` | Проверяет реальную связку, не только эмулятор |
|
||||||
|
| Q10 | refSvcId игнорируем в MVP | validate-cfs всегда OK |
|
||||||
|
|
||||||
|
### Критические точки интеграции (из кода)
|
||||||
|
|
||||||
|
1. **Location обязателен.** `HttpClient.post` достаёт UUID из заголовка `Location`.
|
||||||
|
Мок ОБЯЗАН отдавать `Location: ./<uuid>` на POST /instances и POST /instanceOperations.
|
||||||
|
|
||||||
|
2. **Поллинг спит 5с.** `poll_until_done` делает GET, затем `time.sleep(5)`.
|
||||||
|
При `MOCK_OP_DELAY=0` dtFinish появится на первом же GET — без задержки.
|
||||||
|
|
||||||
|
3. **Точка переключения — auth.py.** `get_client()` → `detect_endpoint()`.
|
||||||
|
Нужен short-circuit: при `NUBES_MOCK=1` возвращать `http://localhost:5001/api/v1/svc`
|
||||||
|
и НЕ вызывать `detect_endpoint`.
|
||||||
|
|
||||||
|
4. **validate-cfs = пустое тело.** Мок отдаёт 200 с пустым телом.
|
||||||
|
|
||||||
|
5. **state.params по коду, cfsParams по числовому id.** YAML должен связывать
|
||||||
|
`svcOperationCfsParamId` ↔ код параметра.
|
||||||
|
|
||||||
|
### План реализации (3 фазы, 12 шагов)
|
||||||
|
|
||||||
|
**Фаза 1 — MVP (create + поллинг):**
|
||||||
|
1. `polygon/defaults.py` — `default_for(dataType)` по типу
|
||||||
|
2. `polygon/config_loader.py` — загрузка `services/*.yaml`, достройка defaults
|
||||||
|
3. `polygon/state.py` — `MockState`: instances, operations, create, run, ленивый dtFinish, reset
|
||||||
|
4. `polygon/server.py` — Flask, префикс `/api/v1/svc`, Location-заголовки
|
||||||
|
5. `polygon/services/dummy.yaml` — Болванка (id параметров из HAR)
|
||||||
|
6. Интеграция в `auth.py`: short-circuit по `NUBES_MOCK`
|
||||||
|
|
||||||
|
**Фаза 2 — полный CRUD + сервисы:**
|
||||||
|
7. GET /instances/{uid} (state.params/state.out), GET /instances (пагинация)
|
||||||
|
8. GET /instanceOperations/default/{id}, GET /services, GET /services/{id}
|
||||||
|
9. `apply_effect`: modify→мерж params, delete→удаление, suspend/resume→статус
|
||||||
|
10. `/_mock/reset` + `polygon/services/postgresql.yaml`
|
||||||
|
|
||||||
|
**Фаза 3 — тесты:**
|
||||||
|
11. `tests/conftest.py` — фикстура поднятия мока + автосброс
|
||||||
|
12. `tests/test_mock_integration.py` — 5 сценариев через `app_client`
|
||||||
|
|
||||||
|
### Структура файлов
|
||||||
|
|
||||||
|
```
|
||||||
|
app-autotest/
|
||||||
|
├── site/
|
||||||
|
│ ├── api/auth.py # +short-circuit localhost (НЕ NUBES_MOCK)
|
||||||
|
│ └── ...
|
||||||
|
└── polygon/
|
||||||
|
├── server.py # Flask-приложение эмулятора
|
||||||
|
├── state.py # MockState (в памяти)
|
||||||
|
├── config_loader.py # загрузка YAML + достройка defaults
|
||||||
|
├── defaults.py # default_for(dataType)
|
||||||
|
└── services/
|
||||||
|
├── dummy.yaml # Болванка
|
||||||
|
└── postgresql.yaml # PostgreSQL (stateOut: users, databases)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Short-circuit: localhost вместо NUBES_MOCK
|
||||||
|
|
||||||
|
Опус ПОДТВЕРДИЛ что проверка `localhost` лучше отдельного флага:
|
||||||
|
|
||||||
|
- `detect_endpoint()` хардкодит dev/test и не читает `NUBES_API_ENDPOINT` — всегда лезет в реальные стенды
|
||||||
|
- При `NUBES_API_ENDPOINT=http://localhost:5001` detect может «угадать» реальный стенд и увести мимо мока
|
||||||
|
- Решение: в `get_client()` и `get_stand()` — если endpoint начинается с `http://localhost` или `http://127.0.0.1` → пропустить detect, сразу использовать endpoint
|
||||||
|
- `stand_name("http://localhost:5001")` → `"?"` — поэтому `get_stand()` при localhost возвращает `"mock"`
|
||||||
|
- Ноль новых env-переменных, существующий `NUBES_API_ENDPOINT` уже в конфиге
|
||||||
|
|
||||||
|
### Реальные ID из HAR для dummy.yaml
|
||||||
|
|
||||||
|
Опус распарсил `development/dummycreate.har` и `development/dummymodify.har`:
|
||||||
|
|
||||||
|
**Операции Болванки (serviceId=1):**
|
||||||
|
|
||||||
|
| operation | svcOperationId |
|
||||||
|
|-----------|----------------|
|
||||||
|
| create | 18 |
|
||||||
|
| delete | 71 |
|
||||||
|
| modify | 92 |
|
||||||
|
| suspend | 93 |
|
||||||
|
| resume | 94 |
|
||||||
|
| redeploy | 240 |
|
||||||
|
|
||||||
|
**Параметры create (svcOperationCfsParamId):**
|
||||||
|
|
||||||
|
| id | код | тип |
|
||||||
|
|----|-----|-----|
|
||||||
|
| 242 | resourceRealm | string (valueList=["dummy"]) |
|
||||||
|
| 198 | durationMs | integer |
|
||||||
|
| 199 | *(код не извлечён)* | boolean |
|
||||||
|
| 200 | *—* | boolean |
|
||||||
|
| 201 | *—* | integer/enum |
|
||||||
|
| 286 | *—* | string |
|
||||||
|
| 321 | *—* | map + dataDescriptor |
|
||||||
|
| 322 | *—* | string |
|
||||||
|
| 396 | *—* | string |
|
||||||
|
| 647 | *—* | map |
|
||||||
|
| 654 | *—* | array(map) |
|
||||||
|
| 863 | *—* | array |
|
||||||
|
|
||||||
|
Для мока коды некритичны (мок сам отдаёт и шаблон и state.params — они должны совпадать между собой). Для реалистичности — код в `mock-architecture-prompt.md` достанет полные коды одной строкой Python.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Универсальный генератор моков из STANDS YAML (Опус, 2026-07-31)
|
||||||
|
|
||||||
|
### Вердикт: универсальный конвертер возможен для всех 37 сервисов
|
||||||
|
|
||||||
|
Опус изучил 6 STANDS YAML (dummy, postgres, k8s, mariadb, VM v2, vDC).
|
||||||
|
Структура единообразна — генерятся одним приложением.
|
||||||
|
STANDS — авторитетный источник (сверен с HAR: create=18, параметры совпадают 1:1).
|
||||||
|
|
||||||
|
### Ключевой маппинг STANDS → polygon
|
||||||
|
|
||||||
|
| STANDS | Polygon | Тип |
|
||||||
|
|--------|---------|-----|
|
||||||
|
| `service_id`→`serviceId`, `service_short_name`→`svcShort` | 1:1 |
|
||||||
|
| `operations[].id/name/action` → `svcOperationId/operation/isCreate` | 1:1 |
|
||||||
|
| `params[].id/code/default/required` → `svcOperationCfsParamId/svcOperationCfsParam/defaultValue/isRequired` | 1:1 |
|
||||||
|
| `params[].data_type` → `dataType` | `html.unescape` |
|
||||||
|
| `params[].sub_params` (list) → `dataDescriptor` (dict по code) | трансформация |
|
||||||
|
| `operations[].kind: subresource` → `stateOut[plural(subresource)]` | трансформация |
|
||||||
|
| `name/service_man/lifecycle/outputs/func/descr/man/sort` | игнор |
|
||||||
|
|
||||||
|
### Алгоритм convert_one()
|
||||||
|
|
||||||
|
1. Базовые поля + операции (фильтр `kind: instance/action`)
|
||||||
|
2. `cfsParams` из create-параметров: unescape, sub_params→dataDescriptor (рекурсивно), default→`default_for(dataType)`
|
||||||
|
3. `stateParams`: scalar→строка, map-fixed→`json.dumps({sub_key: sub_val})`
|
||||||
|
4. `stateOut`: subresource-операции → `{plural(subresource): {}}`
|
||||||
|
|
||||||
|
### 4 границы универсальности
|
||||||
|
|
||||||
|
1. **Контент stateOut** — структура (users/databases) генерится, имена (pgadmin/mydb) — нет. Решение: мок реализует subresource-операции
|
||||||
|
2. **redeploy не у всех** — конвертер включает что есть
|
||||||
|
3. **Динамический valueList** (`func: getAvailableResourceRealms`) — берём статический
|
||||||
|
4. **Битые valueList** — pass-through
|
||||||
|
|
||||||
|
### Архитектура `polygon/from_stands.py`
|
||||||
|
|
||||||
|
```
|
||||||
|
convert_all(stands_dir) → dict[int, config]
|
||||||
|
convert_one(raw) → config
|
||||||
|
_map_param, _build_dd, _gen_state_params, _gen_state_out
|
||||||
|
```
|
||||||
|
|
||||||
|
Поток: `STANDS/*.yaml` → `yaml.safe_load` → `convert_one` → MockState.
|
||||||
|
Вызывается при СТАРТЕ полигона. Ручные YAML (`polygon/services/`) больше не нужны.
|
||||||
|
|
||||||
|
### Subresource-операции в MockState (Опус)
|
||||||
|
|
||||||
|
Обычные `instanceOperations`, без отдельного типа. Разница только в `apply_effect`:
|
||||||
|
|
||||||
|
- `action=="create"` + флаг `subresource` → взять имя из params (`username`/`dbName`),
|
||||||
|
`instance.stateOut[subresource][name] = {}`
|
||||||
|
- `action=="delete"` + `subresource` → `del instance.stateOut[subresource][name]`
|
||||||
|
- иначе → прежняя логика (modify→stateParams, delete→удаление, suspend/resume→статус)
|
||||||
|
|
||||||
|
Конвертер помечает такие операции: `{svcOperationId, operation, subresource, action}`.
|
||||||
|
Один универсальный `apply_effect`, без сервис-специфичного кода.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Полигон — отдельная репа (2026-07-31)
|
||||||
|
|
||||||
|
### Решение: отдельный managed-сервис
|
||||||
|
|
||||||
|
Полигон выносится из `app-autotest/polygon/` в отдельный репозиторий
|
||||||
|
`https://gitea.services.ngcloud.ru/forcloud/polygon.git`.
|
||||||
|
|
||||||
|
Это НЕ часть app-autotest, а самостоятельный сервис:
|
||||||
|
`polygon.pythonk8s.dev.nubes.ru/` — эмулятор Nubes API.
|
||||||
|
|
||||||
|
### Структура (Nubes-совместимая, по howto-flask-nubes.md)
|
||||||
|
|
||||||
|
```
|
||||||
|
polygon/
|
||||||
|
├── requirements.txt
|
||||||
|
├── README.md
|
||||||
|
├── .gitignore
|
||||||
|
└── site/
|
||||||
|
├── app.py # точка входа, порт 5000
|
||||||
|
├── state.py # MockState
|
||||||
|
├── config_loader.py # загрузка YAML + defaults
|
||||||
|
├── defaults.py # default_for(dataType)
|
||||||
|
├── from_stands.py # конвертер STANDS YAML → polygon/services/*.yaml
|
||||||
|
└── services/ # генерится конвертером
|
||||||
|
```
|
||||||
|
|
||||||
|
### Поправки к плану Опуса
|
||||||
|
|
||||||
|
| Было | Стало |
|
||||||
|
|------|-------|
|
||||||
|
| `polygon/server.py` на 5001 | `site/app.py` на 5000 |
|
||||||
|
| Папка внутри app-autotest | Отдельная репа |
|
||||||
|
| `NUBES_API_ENDPOINT=http://localhost:5001` | `http://localhost:5000` (или URL сервиса) |
|
||||||
|
| Только локально/CI | Можно задеплоить на Nubes |
|
||||||
|
|
||||||
|
### Ключевые правила
|
||||||
|
|
||||||
|
- `polygon/` репа = **только код**, никакой документации
|
||||||
|
- Документация полигона → `HISTORY/2026-07-31-session.md` (этот файл)
|
||||||
|
- `polygon/` добавлен в `.gitignore` основного репо
|
||||||
|
- Версионирование своё: polygon v0.1.0
|
||||||
|
- YAML не руками — через `from_stands.py` из STANDS (37 сервисов)
|
||||||
|
- `site/__init__.py` ⛔ нельзя
|
||||||
|
- Импорты без префикса `site.`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Полигон — реализация (2026-07-31, DeepSeek Pro 4)
|
||||||
|
|
||||||
|
### Архитектура утверждена (Sonnet 4.6)
|
||||||
|
|
||||||
|
Промпт Соннету в `polygon-docs/sonnet-polygon-prompt.md`, ответ в `polygon-docs/sonnet-response.md`.
|
||||||
|
Ссылки на генератор YAML: `polygon-docs/tf-provider-refs.md`.
|
||||||
|
|
||||||
|
Ключевые решения Соннета (отличия от Опуса):
|
||||||
|
- subresources: `kind == "subresource"` из STANDS YAML
|
||||||
|
- dtFinish: синхронный в `/run` (sleep → apply → dtFinish)
|
||||||
|
- Лишние операции (restart, recovery): включать все, no-op
|
||||||
|
- stateOut: авто-генерация из subresource-операций
|
||||||
|
- map с sub_params → dataDescriptor для всех типов
|
||||||
|
- 11 тестов (против 5 у Опуса)
|
||||||
|
- UUID: `uuid.uuid4()`
|
||||||
|
- Mock-эндпоинты: +`/_mock/state`, `/_mock/services`, `/_mock/delay`
|
||||||
|
|
||||||
|
Уточнения:
|
||||||
|
- Синхронный dtFinish (блокирует воркер, но MOCK_OP_DELAY ≤ 0.5s)
|
||||||
|
- Вариант Б: сгенерированные YAML коммитятся в репу (НЕ генерить на лету)
|
||||||
|
- `from_stands.py <STANDS_DIR>` — без хардкода, читает все .yaml из директории
|
||||||
|
|
||||||
|
### Сервис запущен на Nubes
|
||||||
|
|
||||||
|
- URL: `https://polygon.pythonk8s.dev.nubes.ru/`
|
||||||
|
- Репа: `https://gitea.services.ngcloud.ru/forcloud/polygon.git`
|
||||||
|
- InstanceUid: `db9d7835-1ef8-4fee-b42f-c91e1c0783cb`
|
||||||
|
- Мониторинг: Grafana (namespace db9d7835...)
|
||||||
|
|
||||||
|
### Этап 1 — from_stands.py (`77f12d2`)
|
||||||
|
|
||||||
|
Создан универсальный конвертер STANDS YAML → polygon config:
|
||||||
|
- `html.unescape()` для всех строк (`>` → `>`, `"` → `"`)
|
||||||
|
- Рекурсивный `dataDescriptor` из `sub_params` (для map, map-fixed, array-map-fixed)
|
||||||
|
- Авто-генерация `state_out_template` из subresource-операций
|
||||||
|
- `stateParams` из create-операции с JSON-генерацией для map-fixed
|
||||||
|
- `cfsParamsByOp` — связка opId → список paramId
|
||||||
|
- 210 строк, 37 YAML сгенерировано, 0 ошибок
|
||||||
|
- Все YAML валидны, HTML entities раскодированы
|
||||||
|
|
||||||
|
### Этап 2 — config_loader + mock_state + state_machine (`1956fb7`)
|
||||||
|
|
||||||
|
**config_loader.py** (50 строк):
|
||||||
|
- Загружает все YAML из `services/`, строит `{svcId: def}` + `{opId: def}`
|
||||||
|
- 37 сервисов, 245 операций в индексе
|
||||||
|
|
||||||
|
**mock_state.py** (150 строк):
|
||||||
|
- `MockState` синглтон: instances, operations, op_params
|
||||||
|
- `create_instance()` → UUID v4, статус "creating"
|
||||||
|
- `create_operation()` → UUID v4, dtFinish=None
|
||||||
|
- `list_instances()` с пагинацией (pageSize≤200, стоп по len<pageSize)
|
||||||
|
- `set_param()`, `get_params()`, `reset()`
|
||||||
|
|
||||||
|
**state_machine.py** (140 строк):
|
||||||
|
- `apply_effect()` — мутирует MockState по kind/action:
|
||||||
|
- instance+create → статус running, stateParams из шаблона, stateOut из шаблона
|
||||||
|
- instance+modify → мерж params через cfsParamsByOp
|
||||||
|
- instance+delete → удалить инстанс
|
||||||
|
- instance+suspend/resume/redeploy → статус
|
||||||
|
- subresource+create → state.out[plural][name] = {}
|
||||||
|
- subresource+delete → del state.out[plural][name]
|
||||||
|
- `_extract_subresource_name()` — ищет имя в op_params, fallback на subresource_name
|
||||||
|
|
||||||
|
### Этап 3 — app.py: 17 эндпоинтов (`d4c2a26`)
|
||||||
|
|
||||||
|
| # | Метод | Путь | Детали |
|
||||||
|
|---|-------|------|--------|
|
||||||
|
| 1 | GET | `/health` | "OK" |
|
||||||
|
| 2 | GET | `/` | HTML: версия, счётчики, delay |
|
||||||
|
| 3 | GET | `/api/v1/svc/services` | список всех сервисов |
|
||||||
|
| 4 | GET | `/api/v1/svc/services/<id>` | операции сервиса |
|
||||||
|
| 5 | GET | `/api/v1/svc/instances` | пагинация (pageSize, page) |
|
||||||
|
| 6 | GET | `/api/v1/svc/instances/<uid>` | полный instance+state |
|
||||||
|
| 7 | POST | `/api/v1/svc/instances` | 201 + Location: ./{uid} |
|
||||||
|
| 8 | GET | `/api/v1/svc/instanceOperations/default/<id>` | cfsParams с dataDescriptor |
|
||||||
|
| 9 | POST | `/api/v1/svc/instanceOperations` | 201 + Location, auto-find create opId |
|
||||||
|
| 10 | GET | `/api/v1/svc/instanceOperations/<uid>` | +cfsParams если ?fields=... |
|
||||||
|
| 11 | POST | `/api/v1/svc/instanceOperationCfsParams` | paramId → value |
|
||||||
|
| 12 | GET | `/api/v1/svc/instanceOperations/<uid>/validate-cfs` | **пустое тело**, 200 |
|
||||||
|
| 13 | POST | `/api/v1/svc/instanceOperations/<uid>/run` | sleep(delay) → apply_effect → dtFinish |
|
||||||
|
| 14 | POST | `/api/v1/svc/_mock/reset` | сброс состояния |
|
||||||
|
| 15 | GET | `/api/v1/svc/_mock/state` | отладка: instances + operations |
|
||||||
|
| 16 | GET | `/api/v1/svc/_mock/services` | отладка: все сервисы |
|
||||||
|
| 17 | POST | `/api/v1/svc/_mock/delay/<s>` | изменить MOCK_OP_DELAY |
|
||||||
|
|
||||||
|
Критические детали:
|
||||||
|
- `default/<int:op_id>` строго ДО `<uid>` в маршрутах Flask
|
||||||
|
- `validate-cfs`: `return "", 200` (НЕ jsonify)
|
||||||
|
- `_now()`: ISO-формат с 'Z'
|
||||||
|
|
||||||
|
### v0.1.0 → v0.2.0 (`4e136cd`)
|
||||||
|
|
||||||
|
Bump версии после трёх этапов изменений.
|
||||||
|
|
||||||
|
### Этап 4 — тесты (`760d16e`)
|
||||||
|
|
||||||
|
**test_converter.py** (10 тестов):
|
||||||
|
- basic_fields, lifecycle, operations_count
|
||||||
|
- html_unescape, value_list, state_params_from_create
|
||||||
|
- data_descriptor_from_sub_params, state_out_subresources
|
||||||
|
- cfs_params_by_op, roundtrip (запись/чтение YAML)
|
||||||
|
|
||||||
|
**test_state_machine.py** (9 тестов):
|
||||||
|
- create → running + params + state.out
|
||||||
|
- delete → инстанс удалён
|
||||||
|
- suspend → suspended
|
||||||
|
- resume → running
|
||||||
|
- modify → params мержатся
|
||||||
|
- create_user → state.out.users[name]
|
||||||
|
- delete_user → users[name] удалён
|
||||||
|
- reset → всё пусто
|
||||||
|
|
||||||
|
**Все 19 тестов PASS.**
|
||||||
|
|
||||||
|
### Проверка в кубере
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh naeel@5.172.178.213 kubectl -n db9d7835... logs deploy/pythonk8s --tail=20
|
||||||
|
```
|
||||||
|
- Под: `1/1 Running`, перезапущен
|
||||||
|
- Логи: все curl-запросы (201, 200), ни одной ошибки
|
||||||
|
|
||||||
|
### Проверка curl (deployed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://polygon.pythonk8s.dev.nubes.ru/health → OK
|
||||||
|
37 services, create flow: 201+Location → run → status=running, params=12
|
||||||
|
```
|
||||||
|
|
||||||
|
### Файлы polygon-docs/ (в корне autotest)
|
||||||
|
|
||||||
|
- `sonnet-polygon-prompt.md` — промпт для Claude Sonnet 4.6
|
||||||
|
- `sonnet-response.md` — полный ответ Соннета (архитектура + план)
|
||||||
|
- `tf-provider-refs.md` — ссылки на генератор YAML в `~/tf_provider`
|
||||||
|
|
||||||
|
### Итоговая структура polygon
|
||||||
|
|
||||||
|
```
|
||||||
|
polygon/
|
||||||
|
├── requirements.txt
|
||||||
|
├── README.md
|
||||||
|
├── .gitignore
|
||||||
|
├── tests/
|
||||||
|
│ ├── test_converter.py # 10 тестов
|
||||||
|
│ └── test_state_machine.py # 9 тестов
|
||||||
|
└── site/
|
||||||
|
├── app.py # 17 эндпоинтов
|
||||||
|
├── mock_state.py # MockState (in-memory)
|
||||||
|
├── state_machine.py # apply_effect()
|
||||||
|
├── config_loader.py # загрузка YAML
|
||||||
|
├── from_stands.py # конвертер STANDS → polygon
|
||||||
|
└── services/ # 37 сгенерированных YAML
|
||||||
|
```
|
||||||
|
|
||||||
|
~900 строк Python, 37 YAML-конфигов, 19 тестов PASS.
|
||||||
|
Задеплоено, проверено через curl и kubectl.
|
||||||
|
Готово к интеграции с app-autotest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Интеграция polygon ↔ app-autotest (2026-07-31, Sonnet + DeepSeek)
|
||||||
|
|
||||||
|
### Промпт Соннету
|
||||||
|
|
||||||
|
Сохранён в `polygon-docs/sonnet-integration-prompt.md`. 6 вопросов.
|
||||||
|
|
||||||
|
### Ответ Соннета
|
||||||
|
|
||||||
|
**Подход:** `endpoint in STANDS` вместо `_is_localhost()`.
|
||||||
|
|
||||||
|
Список STANDS уже есть в `http_client.py`:
|
||||||
|
```python
|
||||||
|
STANDS = [
|
||||||
|
"https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
|
||||||
|
"https://lk-api-gateway-test.ngcloud.ru/api/v1/svc",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `NUBES_API_ENDPOINT` — один из STANDS → автоопределение как раньше.
|
||||||
|
Любой другой URL (localhost, polygon, кастом) → использовать напрямую, без detect.
|
||||||
|
|
||||||
|
Для `stand_name`: `https://polygon.pythonk8s.dev.nubes.ru` содержит "dev" →
|
||||||
|
stand_name вернёт "dev" автоматически. Для localhost → "?" заменяется на "mock".
|
||||||
|
|
||||||
|
**Коллизия портов:** polygon и app-autotest оба на 5000 локально. Решение: polygon на 5001.
|
||||||
|
|
||||||
|
### Реализация (v1.2.24)
|
||||||
|
|
||||||
|
**Файл:** `app-autotest/site/api/auth.py` (единственный файл).
|
||||||
|
|
||||||
|
Изменения:
|
||||||
|
1. Импорт: `from api.http_client import ..., STANDS`
|
||||||
|
2. `get_client()`: `if endpoint in STANDS: detect_endpoint(...)` — автоопределение только для известных стендов
|
||||||
|
3. `get_stand()`: `if endpoint not in STANDS: s = stand_name(endpoint); return s if s != "?" else "mock"`
|
||||||
|
|
||||||
|
**Использование:**
|
||||||
|
```bash
|
||||||
|
# Локально
|
||||||
|
NUBES_API_ENDPOINT=http://localhost:5001/api/v1/svc python app.py
|
||||||
|
|
||||||
|
# Против задеплоенного polygon
|
||||||
|
NUBES_API_ENDPOINT=https://polygon.pythonk8s.dev.nubes.ru/api/v1/svc python app.py
|
||||||
|
|
||||||
|
# Реальные стенды — без изменений
|
||||||
|
python app.py # NUBES_API_ENDPOINT = lk-api-gateway-test... → автоопределение
|
||||||
|
```
|
||||||
|
|
||||||
|
**Версия:** app-autotest v1.2.23 → v1.2.24 (`a398892`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Финальные доработки polygon (2026-07-31, Fable 5 + DeepSeek)
|
||||||
|
|
||||||
|
### Аудиты
|
||||||
|
|
||||||
|
- **Опус:** архитектурный аудит → 5 находок. Главное: публичный деплой vs single-user дизайн.
|
||||||
|
- **Fable 5:** bug hunt → 7 находок. Нашёл то что 3 других ревью пропустили: setdefault не работает, sleep блокирует /health, int(param_id) без try.
|
||||||
|
- **Моё мнение:** Fable vs Opus — `polygon-docs/my-opinion-fable-vs-opus.md`
|
||||||
|
- **Гайд по AI-моделям:** `polygon-docs/ai-models-guide.md`
|
||||||
|
|
||||||
|
### Исправлено (v0.2.5 → v0.3.1)
|
||||||
|
|
||||||
|
| Фикс | От кого |
|
||||||
|
|------|---------|
|
||||||
|
| `Authorization: Bearer` на ВСЕХ эндпоинтах (если MOCK_AUTH_TOKEN задан) | Fable |
|
||||||
|
| Cap DELAY ≤ 5с, min ≥ 0, float() с try/except | Fable |
|
||||||
|
| Runtime warning если WEB_CONCURRENCY != 1 (setdefault удалён) | Fable |
|
||||||
|
| `int(param_id)` с try/except — нет 500 | Fable |
|
||||||
|
| `/_mock/fail-next` — симуляция падения (isSuccessful=False) | Fable |
|
||||||
|
| `.format()` → `%s` в докстринге (KeyError fix) | Я |
|
||||||
|
|
||||||
|
### Интеграционные тесты (app-autotest v1.2.25)
|
||||||
|
|
||||||
|
`test_polygon_integration.py` — 15 тестов (все PASS):
|
||||||
|
- services CRUD, create flow, postgres state.out
|
||||||
|
- 409 при повторном run, modify-мерж, suspend/resume, delete
|
||||||
|
- пагинация, _mock/state, _mock/services, _mock/delay
|
||||||
|
|
||||||
|
### Оптимизация токенов
|
||||||
|
|
||||||
|
- `copilot-instructions.md`: 106 → 25 строк (выброшены повторы, прецеденты)
|
||||||
|
- Гайд по моделям: короткие промпты для Fable (40 строк), полные для Опуса (150 строк)
|
||||||
|
|
||||||
|
### Итог
|
||||||
|
|
||||||
|
- **Polygon:** v0.3.1, задеплоен, 23/23 curl PASS
|
||||||
|
- **App-autotest:** v1.2.25, STANDS-check интеграция
|
||||||
|
- **Готов к последовательному CI.**
|
||||||
|
- **Блокеры для параллельного CI:** per-session модель (Опус), симуляция ошибок (Fable)
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# HISTORY — Хронология сессий
|
||||||
|
|
||||||
|
| Файл | Дата | Содержание |
|
||||||
|
|------|------|------------|
|
||||||
|
| `2026-07-23-session-1.md` | 23.07.2026 | Сессия 1 |
|
||||||
|
| `2026-07-23-session-2.md` | 23.07.2026 | Сессия 2 |
|
||||||
|
| `2026-07-23-session-3.md` | 23.07.2026 | Сессия 3 |
|
||||||
|
| `2026-07-23-session-4.md` | 23.07.2026 | Сессия 4 |
|
||||||
|
| `2026-07-24-all-failures.md` | 24.07.2026 | Все ошибки/падения |
|
||||||
|
| `2026-07-24-har-analysis.md` | 24.07.2026 | Анализ HAR-файлов |
|
||||||
|
| `2026-07-24-session-1.md` | 24.07.2026 | Сессия 1 |
|
||||||
|
| `2026-07-24-session-2.md` | 24.07.2026 | Сессия 2 |
|
||||||
|
| `2026-07-24-session-3.md` | 24.07.2026 | Сессия 3 |
|
||||||
|
| `2026-07-24-session-4.md` | 24.07.2026 | Сессия 4 |
|
||||||
|
| `2026-07-24-session-5.md` | 24.07.2026 | Сессия 5 |
|
||||||
|
| `2026-07-24-session-6.md` | 24.07.2026 | Сессия 6 |
|
||||||
|
| `2026-07-29-session.md` | 29.07.2026 | Сессия: CMDB delete, переписка с Георгием |
|
||||||
|
| `2026-07-30-session.md` | 30.07.2026 | Сессия |
|
||||||
|
| `2026-07-31-session.md` | 31.07.2026 | **Текущая.** Основная сессия: унификация executor (v1.2.0-v1.2.16), аудит Codex (3 раунда, v1.2.19-v1.2.23), архитектура полигона (Опус + Соннет), **реализация polygon v0.2.0** (4 этапа, 17 эндпоинтов, 19 тестов PASS) |
|
||||||
|
|
||||||
|
## Формат
|
||||||
|
|
||||||
|
Каждая сессия содержит:
|
||||||
|
- Контекст (что обсуждалось)
|
||||||
|
- Принятые решения
|
||||||
|
- Реализованные изменения (версии)
|
||||||
|
- Ошибки и как исправлены
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
есть облачный ЛЛМ 120В
|
||||||
|
КРАТКО - где его можно использовать в данном проекте ? и как и зачем
|
||||||
|
|
||||||
|
Список задач обновлен
|
||||||
|
|
||||||
|
Кратко — где 120B-LLM реально полезна в autotest (по убыванию ценности):
|
||||||
|
|
||||||
|
**1. Разбор упавших прогонов (лучшее применение).**
|
||||||
|
Где: История/панель прогона, где есть `error_log` + `stages`.
|
||||||
|
Как: отправить error_log + этапы → LLM возвращает «причина + что проверить».
|
||||||
|
Зачем: тестировщик не читает сырые терраформ/API-логи вручную — экономит время на триаже.
|
||||||
|
|
||||||
|
**2. Генерация сценариев из текста.**
|
||||||
|
Где: редактор сценариев (`scenario-*.js`, `/api/scenario/definitions`).
|
||||||
|
Как: «создай vDC → VM → удали» → LLM генерит `steps[]` JSON по схеме (service_id, operation, output/instance_ref).
|
||||||
|
Зачем: собрать цепочку автотеста словами вместо ручного JSON.
|
||||||
|
|
||||||
|
**3. Подсказка значений параметров.**
|
||||||
|
Где: форма параметров (params-render.js / operations).
|
||||||
|
Как: схема параметров (name, dataType, isRequired, valueList) → LLM предлагает разумные дефолты.
|
||||||
|
Зачем: меньше проб-ошибок при заполнении обязательных полей.
|
||||||
|
|
||||||
|
**4. Анализ трендов по истории.**
|
||||||
|
Где: таблица `runs` / история.
|
||||||
|
Как: агрегат прогонов → LLM: «операция X падает в 40% на стенде test, обычно на этапе Y».
|
||||||
|
Зачем: находить флаки и системные проблемы, а не единичные падения.
|
||||||
|
|
||||||
|
**5. Триаж HAR** (`development/*.har`).
|
||||||
|
Как: HAR → LLM извлекает последовательность API-вызовов/параметры для нового сценария.
|
||||||
|
Зачем: реверс реальных операций Nubes в автотест.
|
||||||
|
|
||||||
|
Архитектурно: отдельный бэкенд-эндпоинт (напр. `/api/llm/analyze`), который дёргает облачный 120B; фронт вызывает по кнопке «Разобрать ошибку»/«Сгенерировать». **Ничего не меняю — это ответ; скажешь «делай», добавлю в план как отдельную фазу.**
|
||||||
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Задача: архитектура эмулятора Nubes API для интеграционных тестов
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
**app-autotest** — Flask-приложение для тестирования сервисов облачной платформы Nubes. Позволяет вручную и через сценарии запускать операции (create/modify/delete/suspend/resume/redeploy) над сервисами через REST API.
|
||||||
|
|
||||||
|
**Проблема:** реальные тесты идут 2-30 минут на операцию, жрут ресурсы облака, недетерминированы. Нужен эмулятор Nubes API чтобы гонять интеграционные тесты мгновенно и бесплатно.
|
||||||
|
|
||||||
|
**Цель:** спроектировать архитектуру эмулятора, который притворяется Nubes API для ЛЮБОГО сервиса, описанного в YAML-конфиге. Не хардкод под каждый сервис, а data-driven подход.
|
||||||
|
|
||||||
|
## Что почитать Опусу (обязательно)
|
||||||
|
|
||||||
|
1. **`DOCS/ARCHITECTURE.md`** — полная архитектура приложения (раздел 8 про безопасность пропустить)
|
||||||
|
2. **`app-autotest/site/api/http_client.py`** — как приложение общается с Nubes API (GET/POST, Location, автостенд)
|
||||||
|
3. **`app-autotest/site/operations/terraform.py`** — логика параметров: cfsParams, normalize_value, resolve_ref_svc, send_params_terraform
|
||||||
|
4. **`app-autotest/site/operations/get_params.py`** — как достаются state.params и state.out из инстанса
|
||||||
|
5. **`app-autotest/site/operations/executor.py`** — единый шлюз запуска операций (это то что будет вызывать эмулятор)
|
||||||
|
6. **`app-autotest/site/operations/poll.py`** — поллинг до dtFinish (эмулятор должен отдавать dtFinish)
|
||||||
|
|
||||||
|
**НЕ читать:** HISTORY, DOCS/* кроме ARCHITECTURE.md, JS-файлы, routes, db. Только backend-ядро.
|
||||||
|
|
||||||
|
## Что должен делать эмулятор
|
||||||
|
|
||||||
|
Принимать те же HTTP-запросы что и реальный Nubes API и отдавать валидные ответы. Ниже — обязательные эндпоинты.
|
||||||
|
|
||||||
|
### 1. Сервисы
|
||||||
|
```
|
||||||
|
GET /services → {results: [{svcId, svc, svcExtendedName, operations}]}
|
||||||
|
GET /services/{id} → {svc: {svc, svcShort, operations: [{svcOperationId, operation, isCreate}]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Инстансы
|
||||||
|
```
|
||||||
|
POST /instances → 201 + Location: ./uuid
|
||||||
|
body: {serviceId, displayName, descr}
|
||||||
|
ответ: {instanceUid}
|
||||||
|
|
||||||
|
GET /instances?pageSize=N&page=P → {results: [{instanceUid, displayName, serviceId, svc, explainedStatus, ...}]}
|
||||||
|
пагинация: pageSize=200 макс, остановка по len(batch) < pageSize
|
||||||
|
|
||||||
|
GET /instances/{uid} → {instance: {instanceUid, displayName, serviceId, svc, explainedStatus, state: {params: {...}, out: {...}}}}
|
||||||
|
state.params — ТЕКУЩИЕ значения параметров (ключ = код, напр. "durationMs": "0")
|
||||||
|
state.out — доп. данные: {users: {pgadmin: {}}, databases: {mydb: {}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Операции
|
||||||
|
```
|
||||||
|
POST /instanceOperations → {instanceOperationUid}
|
||||||
|
body: {instanceUid, svcOperationId, operation}
|
||||||
|
|
||||||
|
POST /instanceOperationCfsParams → {}
|
||||||
|
body: {instanceOperationUid, svcOperationCfsParamId, paramValue}
|
||||||
|
|
||||||
|
GET /instanceOperations/{uid}?fields=dtFinish,isSuccessful,errorLog,duration,stages,svc
|
||||||
|
→ {instanceOperation: {dtFinish, isSuccessful, errorLog, duration, stages, svc}}
|
||||||
|
dtFinish — ключевое: если есть → операция завершена
|
||||||
|
stages: [{stage, dtStart, dtFinish, isSuccessful, duration}, ...]
|
||||||
|
|
||||||
|
POST /instanceOperations/{uid}/run → {}
|
||||||
|
После этого операция считается запущенной, через N секунд появляется dtFinish
|
||||||
|
|
||||||
|
GET /instanceOperations/default/{id} → {svcOperation: {cfsParams: [{svcOperationCfsParamId, svcOperationCfsParam, dataType, isRequired, defaultValue, valueList, refSvcId, dataDescriptor}]}}
|
||||||
|
|
||||||
|
GET /instanceOperations/{uid}/validate-cfs → {} (200 OK, пустое тело = успех)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что эмулятор НЕ должен делать
|
||||||
|
|
||||||
|
- Реально выполнять операции (не Terraform, не Ansible, не K8s)
|
||||||
|
- Проверять валидность параметров (всегда validate-cfs = OK)
|
||||||
|
- Хранить данные в БД (всё в памяти процесса)
|
||||||
|
- Работать с реальными токенами (принимать любой)
|
||||||
|
|
||||||
|
## Ключевые вопросы для проектирования
|
||||||
|
|
||||||
|
### Q1. Конфигурация сервисов
|
||||||
|
|
||||||
|
Формат YAML для описания сервиса должен покрывать:
|
||||||
|
- Базовые поля: svcId, svc (имя), svcExtendedName
|
||||||
|
- Операции: create, modify, delete, suspend, resume, redeploy (у каждого свой svcOperationId)
|
||||||
|
- cfsParams: для КАЖДОГО параметра — id, код, тип, default, valueList, isRequired, refSvcId, dataDescriptor
|
||||||
|
- stateParams: значения по умолчанию после create
|
||||||
|
- stateOut: структура для users/databases (PG) и подобного
|
||||||
|
|
||||||
|
Вопрос: как компактно описать cfsParams для сервисов с 20+ параметрами? Может ли эмулятор сам сгенерировать разумные defaults по типам?
|
||||||
|
|
||||||
|
### Q2. Состояние инстансов
|
||||||
|
|
||||||
|
Эмулятор должен отслеживать:
|
||||||
|
- Какие инстансы созданы (instanceUid → {serviceId, displayName, params, status})
|
||||||
|
- Статус: creating → running (после create), suspending → suspended, modifying → running, deleting → deleted
|
||||||
|
- После delete — инстанс исчезает из GET /instances
|
||||||
|
|
||||||
|
Вопрос: как моделировать explainedStatus? Простая машина состояний или хардкод?
|
||||||
|
|
||||||
|
### Q3. Поллинг и время
|
||||||
|
|
||||||
|
После POST /run операция должна «выполняться» N секунд, потом появляется dtFinish. N можно настраивать (для тестов — 0.1с, для демо — 2с).
|
||||||
|
|
||||||
|
Вопрос: как эмулятор понимает что операция «завершена»? Таймер? Или сразу при запросе проверять время?
|
||||||
|
|
||||||
|
### Q4. Интеграция с app-autotest
|
||||||
|
|
||||||
|
Приложение сейчас использует `NUBES_API_ENDPOINT` из env. При `NUBES_MOCK=1` подставляется `http://localhost:5001/api/v1/svc`.
|
||||||
|
|
||||||
|
Вопрос: нужно ли чтобы эмулятор запускался как отдельный процесс (порт 5001), или можно встроить как Flask blueprint в то же приложение?
|
||||||
|
|
||||||
|
### Q5. Тестовые сценарии
|
||||||
|
|
||||||
|
После создания эмулятора — интеграционные тесты. Минимальный набор:
|
||||||
|
1. create Болванку → проверить instanceUid
|
||||||
|
2. modify параметр → проверить изменение state.params
|
||||||
|
3. delete → проверить исчезновение из GET /instances
|
||||||
|
4. Сценарий: create→modify→delete через run_scenario
|
||||||
|
5. Для PG: create → проверить state.out.users и state.out.databases
|
||||||
|
|
||||||
|
Вопрос: должны ли тесты использовать реальный `app_client` (Flask test client) или напрямую дёргать эмулятор?
|
||||||
|
|
||||||
|
## Ожидаемый ответ
|
||||||
|
|
||||||
|
Структурированный план:
|
||||||
|
1. Архитектура эмулятора (файлы, классы, модули)
|
||||||
|
2. Формат services.yaml с примерами для Болванки и PostgreSQL
|
||||||
|
3. Машина состояний инстансов
|
||||||
|
4. Механизм поллинга (как эмулировать dtFinish)
|
||||||
|
5. Точки интеграции с app-autotest
|
||||||
|
6. План тестов
|
||||||
|
7. Порядок реализации (MVP → полная версия)
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
- Только Python (Flask), никаких новых зависимостей кроме PyYAML
|
||||||
|
- Без БД, всё в памяти
|
||||||
|
- Без многопоточности (один процесс, один пользователь)
|
||||||
|
- Код должен быть ПРОСТЫМ — эмулятор не должен быть сложнее тестируемого приложения
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Задача: универсальный генератор моков из STANDS YAML
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
**app-autotest** — тестирует сервисы Nubes. Для интеграционных тестов проектируется
|
||||||
|
**мок-полигон** — Flask-заглушка, притворяющаяся Nubes API.
|
||||||
|
|
||||||
|
Архитектура полигона уже спроектирована: см. `DOCS/polygon-plan.md`.
|
||||||
|
Ключевое решение: data-driven, сервисы описываются в YAML, эмулятор универсальный.
|
||||||
|
|
||||||
|
**Открытие:** в `STANDS/test/resources_yaml/` лежат 37 YAML-файлов, которые
|
||||||
|
генерит приложение для Terraform-провайдера. Эти YAML описывают ВСЕ сервисы
|
||||||
|
в универсальном формате — провайдер обрабатывает их без привязки к особенностям.
|
||||||
|
|
||||||
|
**Идея:** использовать эти YAML как источник для автоматической генерации
|
||||||
|
конфигов мок-полигона. Не писать YAML вручную — конвертировать из STANDS.
|
||||||
|
|
||||||
|
## Что прочитать Опусу (обязательно)
|
||||||
|
|
||||||
|
1. **`DOCS/polygon-plan.md`** — полный план полигона (369 строк)
|
||||||
|
2. **`STANDS/test/resources_yaml/1_dummy.yaml`** — Болванка (339 строк, простой)
|
||||||
|
3. **`STANDS/test/resources_yaml/90_postgres.yaml`** — PostgreSQL (934 строки, САМЫЙ сложный: 10+ операций, create_user/create_database, map-fixed с sub_params, resourceRealm)
|
||||||
|
4. **`STANDS/test/resources_yaml/150_k8s_sthutrval_cluster.yaml`** — K8s (371 строка, умеренно сложный)
|
||||||
|
5. **`STANDS/test/resources_yaml/115_mariadb.yaml`** — MariaDB (460 строк, похож на PG)
|
||||||
|
6. **`STANDS/test/resources_yaml/27_vc_vm_v2.yaml`** — VM v2 (71 строка, простой)
|
||||||
|
7. **`STANDS/test/resources_yaml/21_vc_vdc.yaml`** — vDC (169 строк, инфраструктурный)
|
||||||
|
|
||||||
|
**НЕ читать:** остальные DOCS, HISTORY, JS-файлы. Только polygon-plan и 6 YAML.
|
||||||
|
|
||||||
|
## Ключевой вопрос
|
||||||
|
|
||||||
|
Можно ли построить **универсальный конвертер** STANDS YAML → polygon-конфиг,
|
||||||
|
который работает для ВСЕХ 37 сервисов без сервис-специфичного кода?
|
||||||
|
|
||||||
|
Если да — спроектировать его архитектуру. Если нет — объяснить где проходит
|
||||||
|
граница универсальности и что придётся делать вручную.
|
||||||
|
|
||||||
|
## Что исследовать
|
||||||
|
|
||||||
|
### 1. Формат STANDS YAML — полнота и единообразие
|
||||||
|
|
||||||
|
Сравни 6 YAML-файлов (от простого dummy до сложного postgres). Ответь:
|
||||||
|
|
||||||
|
- Все ли поля, нужные полигону, присутствуют в STANDS YAML?
|
||||||
|
- Одинакова ли структура у всех 37 сервисов? Есть ли сервисы с нестандартной структурой?
|
||||||
|
- Покрывает ли STANDS YAML все типы параметров: string, integer, boolean, map, map-fixed, array, array(map)?
|
||||||
|
- Есть ли в STANDS YAML информация о valueList, refSvcId, dataDescriptor — или это нужно достраивать?
|
||||||
|
|
||||||
|
### 2. stateParams — генерация из create-операции
|
||||||
|
|
||||||
|
После create полигон должен отдавать `state.params` с ТЕКУЩИМИ значениями.
|
||||||
|
В STANDS YAML у create-операции есть `params[].default` — можно ли их использовать?
|
||||||
|
|
||||||
|
- Для всех ли сервисов create-операция имеет defaults для всех параметров?
|
||||||
|
- Как быть с параметрами без default? Брать из `default_for(dataType)`?
|
||||||
|
- Как мапятся `sub_params` (map-fixed) в state.params? Пример из postgres: `clusterConfiguration.replicas` → как это выглядит в state.params?
|
||||||
|
|
||||||
|
### 3. stateOut — генерация из операций
|
||||||
|
|
||||||
|
Для PostgreSQL `state.out = {users: {pgadmin: {}}, databases: {mydb: {}}}`.
|
||||||
|
|
||||||
|
В STANDS YAML у postgres есть операции `create_user` и `create_database`.
|
||||||
|
Можно ли по наличию таких операций автоматически сгенерировать структуру stateOut?
|
||||||
|
|
||||||
|
- У каких ещё сервисов есть `create_user`/`create_database`? (MariaDB, ClickHouse?)
|
||||||
|
- Есть ли другие паттерны stateOut кроме users/databases?
|
||||||
|
- Можно ли вывести правило: «если есть операция create_X → добавить X в stateOut»?
|
||||||
|
|
||||||
|
### 4. Маппинг полей 1:1
|
||||||
|
|
||||||
|
Составь таблицу маппинга STANDS YAML → polygon-конфиг для ВСЕХ полей.
|
||||||
|
Пример (начало):
|
||||||
|
|
||||||
|
| STANDS YAML | Polygon config |
|
||||||
|
|---|---|
|
||||||
|
| `service_id` | `serviceId` |
|
||||||
|
| `name` | `svc` |
|
||||||
|
| `operations[].id` | `svcOperationId` |
|
||||||
|
| `operations[].name` | `operation` |
|
||||||
|
| `params[].id` | `svcOperationCfsParamId` |
|
||||||
|
| `params[].code` | `svcOperationCfsParam` |
|
||||||
|
| `params[].data_type` | `dataType` |
|
||||||
|
| `params[].sub_params` | `dataDescriptor` |
|
||||||
|
|
||||||
|
Какие поля НЕ мапятся 1:1 и требуют трансформации?
|
||||||
|
|
||||||
|
### 5. Операции — какие включать
|
||||||
|
|
||||||
|
В STANDS YAML у postgres 10+ операций: create, delete, modify, suspend, resume,
|
||||||
|
restart, recovery, create_user, delete_user, create_database, delete_database.
|
||||||
|
|
||||||
|
Полигону нужны 6: create, modify, delete, suspend, resume, redeploy.
|
||||||
|
|
||||||
|
- У всех ли сервисов есть эти 6 операций?
|
||||||
|
- Что делать с «лишними» операциями (restart, recovery, create_user...)?
|
||||||
|
- Нужно ли их включать в мок? Если да — как они влияют на stateParams/stateOut?
|
||||||
|
|
||||||
|
### 6. resourceRealm и refSvcId
|
||||||
|
|
||||||
|
У многих сервисов есть параметр `resourceRealm` (ссылка на платформу k8s/vDC).
|
||||||
|
В полигоне refSvcId в MVP игнорируется (validate-cfs всегда OK).
|
||||||
|
|
||||||
|
- Достаточно ли просто принимать любое значение resourceRealm?
|
||||||
|
- Или нужно чтобы эмулятор возвращал реалистичный valueList для resourceRealm?
|
||||||
|
- Есть ли другие refSvcId-параметры кроме resourceRealm?
|
||||||
|
|
||||||
|
### 7. Краевые случаи
|
||||||
|
|
||||||
|
- Сервисы с `kind: user` или `kind: database` операциями — как их обрабатывать?
|
||||||
|
- Сервисы с `adopt_existing_on_create_default: true` — нужно ли эмулировать adopt?
|
||||||
|
- Есть ли сервисы где create-операция отсутствует (только modify/delete)?
|
||||||
|
- Параметры с `func: getAvailableResourceRealms` — динамический valueList, как эмулировать?
|
||||||
|
|
||||||
|
### 8. Архитектура конвертера
|
||||||
|
|
||||||
|
Если универсальный подход возможен — спроектировать модуль `polygon/from_stands.py`:
|
||||||
|
|
||||||
|
- Какие входы (путь к STANDS, фильтр сервисов)?
|
||||||
|
- Алгоритм конвертации одного YAML → polygon-конфиг
|
||||||
|
- Генерация stateParams (из create defaults + default_for)
|
||||||
|
- Генерация stateOut (из операций create_user/create_database)
|
||||||
|
- Обработка sub_params → dataDescriptor
|
||||||
|
- Что делать с полями которые не мапятся (man, descr, sort, func)?
|
||||||
|
- Выходной формат: один общий JSON/dict или отдельные YAML в `polygon/services/`?
|
||||||
|
|
||||||
|
## Ожидаемый ответ
|
||||||
|
|
||||||
|
1. **Вердикт:** возможен ли универсальный конвертер для всех 37 сервисов?
|
||||||
|
2. **Таблица маппинга:** все поля STANDS → polygon с пометками «1:1», «трансформация», «игнорировать»
|
||||||
|
3. **Алгоритм конвертации:** пошагово, с примерами для dummy и postgres
|
||||||
|
4. **Границы универсальности:** что НЕ покрывается автоматически и требует ручной доработки
|
||||||
|
5. **Архитектура from_stands.py:** структура модуля, функции, поток данных
|
||||||
|
6. **Изменения в polygon-plan.md:** что нужно поправить в существующем плане
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
- Только Python (PyYAML для чтения STANDS)
|
||||||
|
- Результат — данные для MockState, НЕ код самого эмулятора
|
||||||
|
- Конвертер запускается один раз при старте полигона (или offline при сборке)
|
||||||
|
- Никаких внешних API, только чтение локальных YAML-файлов
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# polygon-docs — документация полигона
|
||||||
|
|
||||||
|
Папка для всей документации, не связанной с app-autotest.
|
||||||
|
Никакого кода — только планы, промпты, аудиты.
|
||||||
|
|
||||||
|
## Ключевые файлы
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| [ai-models-guide.md](ai-models-guide.md) | **Гайд по AI-моделям.** Когда использовать Opus/Sonnet/Fable, как составлять промпты. |
|
||||||
|
| [sonnet-polygon-prompt.md](sonnet-polygon-prompt.md) | Промпт Соннету — спроектировать архитектуру полигона |
|
||||||
|
| [sonnet-response.md](sonnet-response.md) | Ответ Соннета — архитектура + план реализации |
|
||||||
|
| [sonnet-code-review-prompt.md](sonnet-code-review-prompt.md) | Промпт Соннету — code review #1 (v0.2.0) |
|
||||||
|
| [sonnet-code-review-2-prompt.md](sonnet-code-review-2-prompt.md) | Промпт Соннету — code review #2 (v0.2.2, после decouple) |
|
||||||
|
| [sonnet-integration-prompt.md](sonnet-integration-prompt.md) | Промпт Соннету — интеграция polygon ↔ app-autotest |
|
||||||
|
| [opus-architecture-audit-prompt.md](opus-architecture-audit-prompt.md) | Промпт Опусу — архитектурный аудит |
|
||||||
|
| [opus-architecture-audit-response.md](opus-architecture-audit-response.md) | Ответ Опуса — 5 находок |
|
||||||
|
| [fable5-architecture-audit-response.md](fable5-architecture-audit-response.md) | Ответ Fable 5 — 7 находок |
|
||||||
|
| [my-opinion-fable-vs-opus.md](my-opinion-fable-vs-opus.md) | Моё мнение — сравнение Опуса и Fable |
|
||||||
|
| [decouple-plan.md](decouple-plan.md) | План развязки монолитного app.py на blueprint'ы |
|
||||||
|
| [tf-provider-refs.md](tf-provider-refs.md) | Ссылки на генератор YAML в `~/tf_provider` |
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Гайд: когда и как использовать Opus, Sonnet, Fable
|
||||||
|
|
||||||
|
Дата: 2026-07-31. Основано на опыте проекта polygon (v0.1.0 → v0.3.0).
|
||||||
|
|
||||||
|
## Кто для чего
|
||||||
|
|
||||||
|
| Модель | Роль | Сильные стороны | Слабые стороны |
|
||||||
|
|--------|------|----------------|----------------|
|
||||||
|
| **Claude Opus** | Архитектор | Видит систему целиком. Находит структурные проблемы. | Может пропустить баги в коде. |
|
||||||
|
| **Claude Sonnet** | Code reviewer | Быстрый. Годится для рутинных ревью. | Поверхностный — 2 ревью пропустили 3 бага которые нашёл Fable. |
|
||||||
|
| **Claude Fable 5** | Bug hunter | Видит баги которые все пропустили. Не требует контекста. | Слаб в архитектурных вопросах. Дороже Опуса в 2 раза. |
|
||||||
|
|
||||||
|
## Результаты на проекте polygon
|
||||||
|
|
||||||
|
| Находка | Опус | Sonnet #1 | Sonnet #2 | Fable 5 |
|
||||||
|
|---------|------|-----------|-----------|---------|
|
||||||
|
| Workers=1 для in-memory | — | 🔴 | — | — |
|
||||||
|
| KeyError в _merge_params | — | 🔴 | — | — |
|
||||||
|
| _cfg.DELAY вместо import | — | — | 🔴 | — |
|
||||||
|
| Публичный деплой vs single-user | 🔴 | — | — | — |
|
||||||
|
| **setdefault не работает** | — | — | — | 🔴 |
|
||||||
|
| **sleep блокирует /health** | — | — | — | 🔴 |
|
||||||
|
| **int(param_id) без try** | — | — | — | 🟡 |
|
||||||
|
| Нет симуляции ошибок | 🟡 | — | — | 🟡 |
|
||||||
|
|
||||||
|
**Вывод:** Fable в одиночку нашёл то, что 3 других ревью (Опус + 2×Соннет) пропустили.
|
||||||
|
|
||||||
|
## Схема использования
|
||||||
|
|
||||||
|
```
|
||||||
|
Новый проект / крупный рефакторинг:
|
||||||
|
1. Опус → архитектурный план
|
||||||
|
2. Реализация (я)
|
||||||
|
3. Sonnet → code review (дешёвый, рутинный)
|
||||||
|
4. Исправления
|
||||||
|
5. Fable → bug hunt (дорогой, но вычищает остатки)
|
||||||
|
|
||||||
|
Мелкие изменения:
|
||||||
|
1. Реализация
|
||||||
|
2. Sonnet → code review
|
||||||
|
```
|
||||||
|
|
||||||
|
## Как составлять промпты
|
||||||
|
|
||||||
|
### Для Опуса (архитектор)
|
||||||
|
|
||||||
|
**Нужно:** полный контекст. Что за проект, какие решения приняты, что уже сделано, куда идём.
|
||||||
|
|
||||||
|
```
|
||||||
|
- Что такое проект (2-3 абзаца)
|
||||||
|
- Текущая архитектура (структура файлов, ключевые решения)
|
||||||
|
- Что сделано (хронология)
|
||||||
|
- Что проверять (конкретные вопросы)
|
||||||
|
- 5-6 вопросов на которые нужен ответ
|
||||||
|
```
|
||||||
|
|
||||||
|
**Размер:** 100-150 строк. **Цена:** дорого, но оправдано — архитектурные ошибки самые дорогие.
|
||||||
|
|
||||||
|
### Для Fable (bug hunter)
|
||||||
|
|
||||||
|
**Нужно:** минимум контекста. Он читает код и находит баги сам.
|
||||||
|
|
||||||
|
```
|
||||||
|
- 2 предложения что это за проект
|
||||||
|
- «Вот код, найди баги которые приведут к:
|
||||||
|
• крашу сервера (500, unhandled exception)
|
||||||
|
• потере/порче данных (гонки, KeyError, wrong state)
|
||||||
|
• дырам в безопасности (открытые эндпоинты, инъекции)
|
||||||
|
• блокировкам (sleep, deadlock, busy-wait)»
|
||||||
|
- «Ответ — только критические находки, без стиля и код-стайла»
|
||||||
|
```
|
||||||
|
|
||||||
|
**Размер:** 30-40 строк. **Экономия:** ~4× меньше токенов vs полный промпт.
|
||||||
|
|
||||||
|
**Проверено:** даже с полным промптом (тем же что для Опуса) Fable нашёл больше багов. Короткий промпт должен дать тот же результат — он смотрит на код, а не на контекст.
|
||||||
|
|
||||||
|
### Для Sonnet (рутинное ревью)
|
||||||
|
|
||||||
|
**Нужно:** структура проекта + на что смотреть.
|
||||||
|
|
||||||
|
```
|
||||||
|
- Структура файлов (таблица)
|
||||||
|
- Что изменилось с прошлого ревью
|
||||||
|
- Конкретные зоны проверки (безопасность, API, импорты, ...)
|
||||||
|
- Формат ответа (🔴🟡🔵)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Размер:** 80-100 строк. **Цена:** самая низкая из трёх.
|
||||||
|
|
||||||
|
## Антипаттерны
|
||||||
|
|
||||||
|
- ❌ Давать Fable полный промпт как Опусу — переплата без пользы
|
||||||
|
- ❌ Давать Опусу короткий промпт как Fable — пропустит архитектурные проблемы
|
||||||
|
- ❌ Пропускать Fable после крупных изменений — баги всплывут в бою
|
||||||
|
- ❌ Использовать Sonnet для архитектурных решений — слишком поверхностный
|
||||||
|
|
||||||
|
## Итог
|
||||||
|
|
||||||
|
| Задача | Кого | Промпт |
|
||||||
|
|--------|------|--------|
|
||||||
|
| Архитектура | Opus | Полный (150 строк) |
|
||||||
|
| Code review | Sonnet | Средний (100 строк) |
|
||||||
|
| Bug hunt | Fable | Короткий (40 строк) |
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# План: Decouple polygon v0.2.1
|
||||||
|
|
||||||
|
Цель: разнести монолитный `app.py` (400 строк, 17 роутов) на отдельные blueprint-файлы
|
||||||
|
по шаблону app-autotest. Никакого инлайн-CSS/HTML — всё в `static/` и `templates/`.
|
||||||
|
|
||||||
|
## Текущее → Целевое
|
||||||
|
|
||||||
|
```
|
||||||
|
polygon/site/
|
||||||
|
├── app.py (400 строк, всё в одном)
|
||||||
|
├── mock_state.py
|
||||||
|
├── state_machine.py
|
||||||
|
├── config_loader.py
|
||||||
|
└── from_stands.py
|
||||||
|
|
||||||
|
↓
|
||||||
|
|
||||||
|
polygon/site/
|
||||||
|
├── app.py # ТОЛЬКО: Flask(), register_blueprint, /health, app.run()
|
||||||
|
├── routes/
|
||||||
|
│ ├── root.py # GET / (render_template + style.css)
|
||||||
|
│ ├── services_routes.py # GET /api/v1/svc/services, GET /services/<id>
|
||||||
|
│ ├── instances_routes.py # GET/POST /api/v1/svc/instances, GET /instances/<uid>
|
||||||
|
│ ├── operations_routes.py # POST /instanceOperations, GET default, GET status, POST params, GET validate
|
||||||
|
│ ├── run.py # POST /instanceOperations/<uid>/run
|
||||||
|
│ └── mock_routes.py # POST /_mock/reset, GET /_mock/state, GET /_mock/services, POST /_mock/delay
|
||||||
|
├── state/
|
||||||
|
│ ├── mock_state.py # MockState (без изменений)
|
||||||
|
│ └── state_machine.py # apply_effect (без изменений)
|
||||||
|
├── config/
|
||||||
|
│ └── loader.py # load_services() (бывший config_loader.py)
|
||||||
|
├── converter/
|
||||||
|
│ └── from_stands.py # конвертер (без изменений)
|
||||||
|
├── utils/
|
||||||
|
│ ├── pluralize.py # _pluralize() — одна функция
|
||||||
|
│ └── now.py # _now() — одна функция
|
||||||
|
├── static/
|
||||||
|
│ └── style.css # CSS из index() — тёмная тема
|
||||||
|
├── templates/
|
||||||
|
│ └── index.html # HTML из index() — Jinja2 с {{ VERSION }}
|
||||||
|
└── services/
|
||||||
|
└── ...yaml # без изменений
|
||||||
|
```
|
||||||
|
|
||||||
|
## Пошагово
|
||||||
|
|
||||||
|
### Шаг 1. Утилиты
|
||||||
|
|
||||||
|
- `utils/now.py` — `_now()` из app.py (одна функция)
|
||||||
|
- `utils/pluralize.py` — `_pluralize()` из state_machine.py (одна функция)
|
||||||
|
|
||||||
|
### Шаг 2. CSS и HTML
|
||||||
|
|
||||||
|
- `static/style.css` — вынести инлайн-CSS из f-строки `index()`
|
||||||
|
- `templates/index.html` — вынести HTML из f-строки, использовать Jinja2 `{{ version }}`, `<link rel="stylesheet">`
|
||||||
|
|
||||||
|
### Шаг 3. Маршруты — 6 blueprint-файлов
|
||||||
|
|
||||||
|
Каждый blueprint импортирует нужные модули из `state/`, `config/`, `utils/`.
|
||||||
|
|
||||||
|
- `routes/root.py` — `bp = Blueprint("root", __name__)`, роут `/` с `render_template`
|
||||||
|
- `routes/services_routes.py` — `bp = Blueprint("services", __name__)`, 2 роута
|
||||||
|
- `routes/instances_routes.py` — `bp = Blueprint("instances", __name__)`, 3 роута
|
||||||
|
- `routes/operations_routes.py` — `bp = Blueprint("operations", __name__)`, 5 роутов
|
||||||
|
- `routes/run.py` — `bp = Blueprint("run", __name__)`, 1 роут
|
||||||
|
- `routes/mock_routes.py` — `bp = Blueprint("mock", __name__)`, 4 роута
|
||||||
|
|
||||||
|
### Шаг 4. app.py — только скелет
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
from flask import Flask
|
||||||
|
from routes.root import bp as root_bp
|
||||||
|
from routes.services_routes import bp as services_bp
|
||||||
|
# ... все 6 blueprint'ов
|
||||||
|
from config.loader import load_services
|
||||||
|
|
||||||
|
os.environ.setdefault("WEB_CONCURRENCY", "1")
|
||||||
|
VERSION = "0.2.1"
|
||||||
|
MOCK_OP_DELAY = float(os.getenv("MOCK_OP_DELAY", "0.1"))
|
||||||
|
|
||||||
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||||
|
|
||||||
|
app.register_blueprint(root_bp)
|
||||||
|
app.register_blueprint(services_bp)
|
||||||
|
# ... все 6
|
||||||
|
|
||||||
|
@app.route("/health")
|
||||||
|
def health():
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True, host="0.0.0.0", port=5000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 5. Импорты в state_machine и from_stands
|
||||||
|
|
||||||
|
- `state_machine.py`: `from utils.pluralize import pluralize` (вместо `_pluralize()` внутри)
|
||||||
|
- `from_stands.py`: импортирует `pluralize` из `utils/`
|
||||||
|
|
||||||
|
## Что НЕ меняется
|
||||||
|
|
||||||
|
- `state/mock_state.py` — как есть
|
||||||
|
- `state/state_machine.py` — только импорт `pluralize`
|
||||||
|
- `converter/from_stands.py` — только импорт `pluralize`
|
||||||
|
- `services/*.yaml` — данные
|
||||||
|
- `tests/` — только поправить пути импорта (site → site.state и т.д.)
|
||||||
|
|
||||||
|
## Верификация
|
||||||
|
|
||||||
|
1. `python -m py_compile` на ВСЕХ .py файлах
|
||||||
|
2. `python -c "from app import app; [print(r.rule) for r in app.url_map.iter_rules()]"` → те же 17 роутов
|
||||||
|
3. `pytest tests/ -v` → 19/19 PASS
|
||||||
|
4. Запустить `python app.py` → curl /health, /services, /instances → работает
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Архитектурный аудит polygon v0.2.5 — ответ Claude Fable 5
|
||||||
|
|
||||||
|
Дата: 2026-07-31
|
||||||
|
|
||||||
|
## Итоговая оценка
|
||||||
|
|
||||||
|
Архитектура соразмерна задаче и в целом здоровая. Blueprint'ы, data-driven конфиги, единый config/loader.py, синхронный run — правильный выбор. Код читаемый, докстринги честные, критичные контракты задокументированы.
|
||||||
|
|
||||||
|
Главный структурный риск: сервис задеплоен как публичный shared-эндпоинт, но спроектирован как эксклюзивный однопользовательский стенд.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Находки
|
||||||
|
|
||||||
|
### 🔴 1. Основной API полностью открыт на публичном домене
|
||||||
|
MOCK_AUTH_TOKEN защищает только /_mock/*. POST /instances, /instanceOperations, run — без проверки.
|
||||||
|
|
||||||
|
### 🔴 2. sleep(DELAY) блокирует /health → риск рестарта контейнера
|
||||||
|
1 sync-воркер, sleep в run замораживает весь сервер включая /health. Комбинация delay=30 + несколько run → healthcheck не отвечает → Nubes рестартит под → состояние потеряно.
|
||||||
|
|
||||||
|
### 🔴 3. Гарантия «1 воркер» через setdefault — не работает
|
||||||
|
os.environ.setdefault("WEB_CONCURRENCY", "1") выполняется при импорте app, а gunicorn читает WEB_CONCURRENCY при старте мастера — до импорта. Если платформа выставит WEB_CONCURRENCY=4, будет 4 воркера и 4 несвязанных MockState.
|
||||||
|
|
||||||
|
### 🟡 4. 500-ки на невалидном вводе
|
||||||
|
set_param: int(param_id) без try → ValueError → 500.
|
||||||
|
/_mock/delay: float("garbage") → 500. Отрицательные значения принимаются молча.
|
||||||
|
|
||||||
|
### 🟡 5. _extract_subresource_name — хрупкий
|
||||||
|
Берёт первое непустое строковое значение. Порядок = порядок set_param. Если executor отправит пароль раньше имени — subresource назовётся паролем.
|
||||||
|
|
||||||
|
### 🟡 6. Мок слишком «добрый»
|
||||||
|
- validate-cfs всегда 200 (required не проверяются)
|
||||||
|
- modify принимает isModifiable: false
|
||||||
|
- операция на creating-инстансе (modify до завершения create)
|
||||||
|
- svcOperationId не сверяется с serviceId инстанса
|
||||||
|
- isSuccessful всегда True — негативные сценарии нельзя тестировать
|
||||||
|
|
||||||
|
### 🟢 7. Мелочи
|
||||||
|
- operations/op_params не чистятся (кроме reset)
|
||||||
|
- stages: [] всегда пуст
|
||||||
|
- total в пагинации — проверить с реальным API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ответы на вопросы
|
||||||
|
|
||||||
|
**Q1. In-memory vs Redis.** Redis не нужен. Потеря состояния при рестарте — фича.
|
||||||
|
|
||||||
|
**Q2. Параллельные тесты.** (A) polygon per-session (рекомендовано) или (B) namespace-изоляция.
|
||||||
|
|
||||||
|
**Q3. Генерация YAML при старте.** Нет. Оффлайн + drift-check тест.
|
||||||
|
|
||||||
|
**Q4. Мониторинг.** Prometheus избыточен. Достаточно логов + /_mock/state.
|
||||||
|
|
||||||
|
**Q5. Готовность к CI.** К последовательному — готов. Блокеры: auth API (1), cap DELAY (2), симуляция ошибок (6).
|
||||||
|
|
||||||
|
## Приоритеты
|
||||||
|
|
||||||
|
1. Токен на весь API + cap DELAY — дёшево, закрывает оба 🔴
|
||||||
|
2. /_mock/fail-next — открывает негативные тесты
|
||||||
|
3. Runtime-проверка воркеров + фикс int(param_id)
|
||||||
|
4. Модель владения (per-session) — отложить
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Моё мнение — сравнение аудитов Опуса и Fable 5
|
||||||
|
|
||||||
|
Дата: 2026-07-31
|
||||||
|
|
||||||
|
## Кто что нашёл
|
||||||
|
|
||||||
|
| Находка | Опус | Fable 5 |
|
||||||
|
|---------|------|---------|
|
||||||
|
| Auth на основном API | 🔴 | 🔴 |
|
||||||
|
| sleep блокирует /health → pod restart | — | 🔴 |
|
||||||
|
| setdefault не работает (gunicorn timing) | — | 🔴 |
|
||||||
|
| int(param_id) без try → 500 | — | 🟡 |
|
||||||
|
| modify на creating-инстансе | — | 🟡 |
|
||||||
|
| Публичный деплой vs single-user | 🔴 | — |
|
||||||
|
| _extract_subresource_name хрупкий | 🟡 | 🟡 |
|
||||||
|
| Память не чистится | 🟡 | 🟢 |
|
||||||
|
| Нет симуляции ошибок | 🟡 | 🟡 |
|
||||||
|
| Синхронный sleep vs ленивый dtFinish | 🟡 | — |
|
||||||
|
| stages: [] всегда пуст | — | 🟢 |
|
||||||
|
|
||||||
|
## Оценка
|
||||||
|
|
||||||
|
**Опус** — архитектор. Смотрит на систему сверху: «правильно ли спроектировано под задачу?». Нашёл структурный разрыв (публичный деплой vs однопользовательский дизайн), дал стратегические рекомендации (per-session модель).
|
||||||
|
|
||||||
|
**Fable 5** — инженер. Смотрит на код снизу: «что сломается при эксплуатации?». Нашёл три бага которые никто не заметил:
|
||||||
|
- `setdefault("WEB_CONCURRENCY", "1")` — не работает (gunicorn читает env ДО импорта app)
|
||||||
|
- `sleep(DELAY)` блокирует `/health` → если delay > healthcheck timeout → Nubes рестартит под
|
||||||
|
- `int(param_id)` без try → 500 на кривом JSON
|
||||||
|
|
||||||
|
## Что делать
|
||||||
|
|
||||||
|
Приоритет Fable прагматичнее: две строчки кода (cap DELAY + токен) спасут от краша пода. Приоритет Опуса стратегический: per-session модель нужна для параллельного CI, но это потом.
|
||||||
|
|
||||||
|
**Порядок:** Fable → Опус. Сначала закрыть риски эксплуатации, потом архитектурные улучшения.
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# Архитектурный аудит polygon v0.2.5
|
||||||
|
|
||||||
|
> Адресат: Claude Opus (новый чат)
|
||||||
|
> Дата: 2026-07-31
|
||||||
|
> ⛔ ОТВЕТ — ТОЛЬКО В ЧАТ. Не редактировать файлы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что такое polygon
|
||||||
|
|
||||||
|
**Polygon** — отдельный managed-сервис (`polygon.pythonk8s.dev.nubes.ru`),
|
||||||
|
эмулирующий REST API облачной платформы Nubes. Нужен для интеграционных тестов
|
||||||
|
приложения **app-autotest** — чтобы тесты гонялись не на реальном облаке, а на
|
||||||
|
эмуляторе.
|
||||||
|
|
||||||
|
- Flask 3.0 + gunicorn (1 воркер), деплой на Nubes pythonk8s
|
||||||
|
- Всё состояние в памяти (MockState), без БД
|
||||||
|
- Data-driven: конфиги сервисов генерируются из 37 STANDS YAML через `from_stands.py`
|
||||||
|
- 17 API-эндпоинтов с префиксом `/api/v1/svc`
|
||||||
|
- 19 юнит-тестов (pytest) + 15 интеграционных (в app-autotest)
|
||||||
|
- Версия: **v0.2.5**, задеплоена и протестирована curl'ом
|
||||||
|
|
||||||
|
## 2. Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
polygon/
|
||||||
|
├── requirements.txt # Flask>=3.0, gunicorn>=21.2, PyYAML>=6.0
|
||||||
|
├── tests/
|
||||||
|
│ ├── test_converter.py # 10 юнит-тестов from_stands.py
|
||||||
|
│ └── test_state_machine.py # 9 юнит-тестов state_machine.py
|
||||||
|
└── site/
|
||||||
|
├── app.py # 57 строк: Flask() + 6 blueprint'ов + app.run()
|
||||||
|
├── routes/
|
||||||
|
│ ├── root.py # /health, / (Jinja2)
|
||||||
|
│ ├── 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/*
|
||||||
|
├── mock_state.py # MockState: instances, operations, op_params
|
||||||
|
├── state_machine.py # apply_effect() — мутация состояния
|
||||||
|
├── config/
|
||||||
|
│ └── loader.py # SERVICES, OPS_INDEX, DELAY, VERSION
|
||||||
|
├── utils/
|
||||||
|
│ ├── now.py # now() — UTC ISO с 'Z'
|
||||||
|
│ └── pluralize.py # pluralize()
|
||||||
|
├── from_stands.py # конвертер STANDS YAML → polygon config
|
||||||
|
├── static/style.css # тёмная тема
|
||||||
|
├── templates/index.html # Jinja2-шаблон
|
||||||
|
└── services/ # 37 сгенерированных YAML-конфигов
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ключевые архитектурные решения:**
|
||||||
|
|
||||||
|
| Решение | Обоснование |
|
||||||
|
|---------|-------------|
|
||||||
|
| Blueprint'ы по доменам | По шаблону app-autotest. 6 файлов вместо монолитного app.py (было 418 строк → 57) |
|
||||||
|
| In-memory state, 1 воркер | Без БД, без гонок. `WEB_CONCURRENCY=1` принудительно |
|
||||||
|
| Синхронный dtFinish | `sleep(MOCK_OP_DELAY)` прямо в `/run` — без потоков, просто |
|
||||||
|
| Data-driven из STANDS YAML | 37 сервисов генерится конвертером, не хардкод |
|
||||||
|
| Модульные переменные в config/loader.py | SERVICES/OPS_INDEX/DELAY/VERSION — единый источник |
|
||||||
|
| `import config.loader as _cfg` | Модульные переменные меняются на лету (`_cfg.DELAY = x`) |
|
||||||
|
| CSS/HTML отделены от Python | `static/style.css` + `templates/index.html` — не в f-строках |
|
||||||
|
|
||||||
|
## 3. Поток данных
|
||||||
|
|
||||||
|
```
|
||||||
|
STANDS YAML (37 файлов)
|
||||||
|
→ from_stands.py (html.unescape, sub_params→dataDescriptor, stateOut auto-gen)
|
||||||
|
→ services/*.yaml (37 конфигов)
|
||||||
|
→ config/loader.py (_load_all при импорте)
|
||||||
|
→ SERVICES + OPS_INDEX (модульные переменные)
|
||||||
|
→ routes/*.py (читают SERVICES/OPS_INDEX/DELAY)
|
||||||
|
→ MockState (create_instance → create_operation → run → apply_effect)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Что сделано (хронология)
|
||||||
|
|
||||||
|
- **Этап 1:** `from_stands.py` — конвертер STANDS → polygon, 37 YAML
|
||||||
|
- **Этап 2:** `mock_state.py` + `state_machine.py` + `config/loader.py`
|
||||||
|
- **Этап 3:** `app.py` — 17 эндпоинтов (потом разнесены на blueprint'ы)
|
||||||
|
- **Code Review #1 (Sonnet):** 12 находок, 2 крит. исправлены (workers=1, KeyError в _merge_params)
|
||||||
|
- **Decouple:** монолит → blueprint'ы + CSS/HTML разделение
|
||||||
|
- **Code Review #2 (Sonnet):** 9 находок — `_cfg.DELAY`, VERSION в config/loader, мёртвые импорты
|
||||||
|
- **Интеграция с app-autotest:** `auth.py` — STANDS-check, v1.2.24
|
||||||
|
- **🟡 фиксы:** `json.dumps` вместо ручного JSON, 409 при повторном run, `MOCK_AUTH_TOKEN`
|
||||||
|
- **Интеграционные тесты:** 15 тестов в app-autotest, все PASS
|
||||||
|
|
||||||
|
## 5. Что проверять
|
||||||
|
|
||||||
|
### Архитектура
|
||||||
|
- Правильно ли выбран паттерн blueprint'ов? Не переусложнено ли?
|
||||||
|
- Модульные переменные в `config/loader.py` — адекватный подход или есть лучше?
|
||||||
|
- In-memory state с 1 воркером — масштабируемо ли для CI (параллельные тесты)?
|
||||||
|
- Есть ли архитектурные дыры: что будет при 1000 инстансов? При рестарте сервера?
|
||||||
|
|
||||||
|
### Поток данных
|
||||||
|
- Не теряются ли данные между этапами: STANDS → YAML → loader → state?
|
||||||
|
- Все ли поля маппятся корректно? (dataDescriptor, valueList, stateOut)
|
||||||
|
- Правильно ли обрабатываются subresources (create_user, create_database)?
|
||||||
|
|
||||||
|
### API-совместимость
|
||||||
|
- Все ли форматы ответов совпадают с реальным Nubes API?
|
||||||
|
- Location-заголовки, пустое тело validate-cfs, 201/404/409 коды?
|
||||||
|
- Пагинация: стоп по `len < pageSize`, cap 200?
|
||||||
|
|
||||||
|
### Безопасность
|
||||||
|
- `_mock/*` защищены `MOCK_AUTH_TOKEN` — достаточно?
|
||||||
|
- Нет ли утечек данных между тестами (autouse reset в conftest)?
|
||||||
|
- Что будет при отправке невалидного JSON в POST /instances?
|
||||||
|
|
||||||
|
### Что дальше
|
||||||
|
- Готов ли polygon к CI/CD?
|
||||||
|
- Что нужно для продакшен-использования (не только тесты)?
|
||||||
|
- Какие мониторинг/логирование нужны?
|
||||||
|
|
||||||
|
## 6. Вопросы к Опусу
|
||||||
|
|
||||||
|
### Q1. In-memory vs Redis
|
||||||
|
Сейчас всё в `MockState.instances` (dict). При рестарте сервера всё теряется.
|
||||||
|
Для CI это ок (тесты стартуют заново). Нужен ли персистентный слой? Redis? Файлы?
|
||||||
|
|
||||||
|
### Q2. Масштабирование
|
||||||
|
1 воркер, 1 процесс. Если запустить параллельные тесты (несколько pytest-сессий) —
|
||||||
|
каждая поднимет свой polygon на своём порту? Или один общий сервер?
|
||||||
|
|
||||||
|
### Q3. Генерация YAML
|
||||||
|
Сейчас `from_stands.py` запускается вручную, результат коммитится. Стоит ли
|
||||||
|
генерить YAML при старте сервера (если `STANDS_DIR` задан)?
|
||||||
|
|
||||||
|
### Q4. Мониторинг
|
||||||
|
Нужны ли метрики: количество инстансов, операций, latency? Prometheus-экспорт?
|
||||||
|
Или только Grafana-логи как сейчас?
|
||||||
|
|
||||||
|
### Q5. Общая оценка
|
||||||
|
Готов ли polygon к использованию в CI app-autotest? Что критично доделать?
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Архитектурный аудит polygon v0.2.5 — ответ Опуса
|
||||||
|
|
||||||
|
Дата: 2026-07-31
|
||||||
|
|
||||||
|
## Итоговая оценка
|
||||||
|
|
||||||
|
Архитектура здоровая и соразмерная задаче. Blueprint-разбиение, data-driven из STANDS YAML, синхронный run без потоков, единый config/loader — правильные решения.
|
||||||
|
|
||||||
|
Но есть **структурный разрыв**: сервис задеплоен как публичный managed-сервис, при этом спроектирован как эксклюзивный однопользовательский in-memory стенд.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Находки
|
||||||
|
|
||||||
|
### 🔴 1. Публичный деплой + общий global state → изоляция тестов ломается в параллельном CI
|
||||||
|
|
||||||
|
Две параллельные pytest-сессии делят одно состояние. `reset()` одной сессии стирает инстансы другой.
|
||||||
|
|
||||||
|
**Решение:** namespace-изоляция по `X-Test-Session: <uuid>` ИЛИ per-session контейнер в CI.
|
||||||
|
|
||||||
|
### 🔴 2. Основной API без аутентификации на публичном домене
|
||||||
|
|
||||||
|
`MOCK_AUTH_TOKEN` защищает только `/_mock/*`. POST /instances, /instanceOperations, run — открыты полностью.
|
||||||
|
|
||||||
|
**Решение:** `before_request` на уровне app (проверка `Authorization: Bearer <любой>`) или сетевое ограничение.
|
||||||
|
|
||||||
|
### 🟡 3. Синхронный sleep блокирует весь сервер
|
||||||
|
|
||||||
|
При `--workers 1`, `time.sleep(DELAY)` в run сериализует ВСЕ запросы. При `delay=5` сервер заморожен на 5 секунд.
|
||||||
|
|
||||||
|
Конфликтует с исходным планом Опуса (решение Q4 — «ленивый dtFinish»). Реализация ушла в синхронный sleep.
|
||||||
|
|
||||||
|
### 🟡 4. _extract_subresource_name — хрупкий выбор имени
|
||||||
|
|
||||||
|
Берётся первое непустое строковое значение в op_params. Порядок параметров не гарантирует что имя пойдёт раньше пароля.
|
||||||
|
|
||||||
|
**Решение:** маппить по коду параметра через cfsParams.
|
||||||
|
|
||||||
|
### 🟡 5. Память не ограничена + нет симуляции ошибок
|
||||||
|
|
||||||
|
- Операции и op_params не чистятся никогда → медленная утечка
|
||||||
|
- run всегда `isSuccessful=True` → мок не умеет эмулировать падение
|
||||||
|
- Нет cap на число инстансов
|
||||||
|
|
||||||
|
## Ответы на вопросы
|
||||||
|
|
||||||
|
**Q1. In-memory vs Redis.** Persistence не нужен для CI. Рестарт = чистый стенд, это фича.
|
||||||
|
|
||||||
|
**Q2. Масштабирование.** Безопасно только при эксклюзивном владении. Рекомендация: polygon per-session в CI (снимает находки 1, 2, 3 разом).
|
||||||
|
|
||||||
|
**Q3. Генерация YAML при старте.** Не надо. Оставить offline. Добавить CI-проверку «from_stands.py даёт тот же результат».
|
||||||
|
|
||||||
|
**Q4. Мониторинг.** Prometheus избыточен. Достаточно `/_mock/state` + структурные логи.
|
||||||
|
|
||||||
|
**Q5. Готовность к CI.** К последовательному CI — готов. Блокеры для параллельного: изоляция сессий (1), auth API (2), симуляция ошибок (5).
|
||||||
|
|
||||||
|
## Приоритеты
|
||||||
|
|
||||||
|
1. Решить модель владения — per-session контейнер ИЛИ namespace-изоляция
|
||||||
|
2. Симуляция ошибок операций — для негативных тестов
|
||||||
|
3. Auth-гейт основного API
|
||||||
|
4. Маппинг subresource-имён по коду параметра
|
||||||
|
5. Косметика версии
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Code Review #2: polygon v0.2.2 (после decouple)
|
||||||
|
|
||||||
|
> Адресат: Claude Sonnet 4.6 (новый чат)
|
||||||
|
> Дата: 2026-07-31
|
||||||
|
> Предыдущее ревью: v0.2.0 (12 находок, 2 крит. исправлены)
|
||||||
|
> ⛔ ОТВЕТ — ТОЛЬКО В ЧАТ. Не редактировать файлы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
После первого ревью (v0.2.0) были исправлены 2 критических бага и проведён
|
||||||
|
decouple-рефакторинг: монолитный `app.py` (418 строк) разнесён на blueprint'ы
|
||||||
|
по шаблону app-autotest. CSS и HTML вынесены из Python-строк в отдельные файлы.
|
||||||
|
|
||||||
|
Актуальная версия: **v0.2.2**, задеплоена на `polygon.pythonk8s.dev.nubes.ru`.
|
||||||
|
23/23 curl-тестов PASS, 19/19 pytest PASS.
|
||||||
|
|
||||||
|
## Новая структура
|
||||||
|
|
||||||
|
```
|
||||||
|
polygon/site/
|
||||||
|
├── app.py # 57 строк: Flask(), 6 blueprint'ов, app.run()
|
||||||
|
├── routes/
|
||||||
|
│ ├── root.py # /health, / (Jinja2 render_template)
|
||||||
|
│ ├── services_routes.py # GET /api/v1/svc/services, /services/<id>
|
||||||
|
│ ├── instances_routes.py # GET/POST /api/v1/svc/instances, GET /instances/<uid>
|
||||||
|
│ ├── operations_routes.py # /instanceOperations/* (5 эндпоинтов: default, create, status, params, validate)
|
||||||
|
│ ├── run.py # POST /instanceOperations/<uid>/run
|
||||||
|
│ └── mock_routes.py # /_mock/* (reset, state, services, delay)
|
||||||
|
├── state/
|
||||||
|
│ ├── mock_state.py # MockState (in-memory, UUID v4, пагинация)
|
||||||
|
│ └── state_machine.py # apply_effect() + _merge_params()
|
||||||
|
├── config/
|
||||||
|
│ └── loader.py # load_services() + модульные переменные SERVICES/OPS_INDEX/DELAY
|
||||||
|
├── utils/
|
||||||
|
│ ├── now.py # now() — UTC ISO с 'Z'
|
||||||
|
│ └── pluralize.py # pluralize() — одна функция
|
||||||
|
├── converter/
|
||||||
|
│ └── from_stands.py # конвертер STANDS YAML → polygon config
|
||||||
|
├── static/
|
||||||
|
│ └── style.css # тёмная тема (из f-строки)
|
||||||
|
├── templates/
|
||||||
|
│ └── index.html # Jinja2-шаблон (из f-строки)
|
||||||
|
└── services/
|
||||||
|
└── 37 YAML-конфигов
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что изменилось с прошлого ревью
|
||||||
|
|
||||||
|
| v0.2.0 | v0.2.2 |
|
||||||
|
|--------|--------|
|
||||||
|
| `app.py` 418 строк, всё в одном | `app.py` 57 строк, только скелет |
|
||||||
|
| 17 `@app.route(...)` в одном файле | 7 blueprint-файлов в `routes/` |
|
||||||
|
| CSS в f-строке `index()` | `static/style.css` |
|
||||||
|
| HTML в f-строке `index()` | `templates/index.html` (Jinja2) |
|
||||||
|
| `_now()` в app.py + mock_state.py | `utils/now.py` |
|
||||||
|
| `_pluralize()` в state_machine + from_stands | `utils/pluralize.py` |
|
||||||
|
| `config_loader.py` | `config/loader.py` (модульные переменные) |
|
||||||
|
| `--workers 2` в docstring | `--workers 1` + `WEB_CONCURRENCY=1` |
|
||||||
|
| `cfs_params[pid]` → KeyError | `cfs_params.get(pid)` → безопасно |
|
||||||
|
|
||||||
|
## Что проверять
|
||||||
|
|
||||||
|
### 1. Корректность blueprint-регистрации
|
||||||
|
- Все 17+ маршрутов на месте?
|
||||||
|
- Порядок `default/<int>` перед `<uid>` сохранён в operations_routes.py?
|
||||||
|
- Нет коллизий имён blueprint'ов?
|
||||||
|
- `url_prefix` не дублируется с путями в `@bp.route()`?
|
||||||
|
|
||||||
|
### 2. Импорты и зависимости
|
||||||
|
- Нет циклических импортов между модулями?
|
||||||
|
- `config/loader.py` — модульные переменные инициализируются ровно один раз?
|
||||||
|
- `routes/run.py` и `routes/mock_routes.py` правильно работают с `config.loader.DELAY` через `_cfg.DELAY = seconds`?
|
||||||
|
- Старый `config_loader.py` удалён — нигде не осталось импортов?
|
||||||
|
|
||||||
|
### 3. Порядок вызова функций
|
||||||
|
- При дроблении не нарушен ли порядок: validate → set_param → run → apply_effect?
|
||||||
|
- `apply_effect` вызывается с правильными аргументами (mock_state.state, SERVICES)?
|
||||||
|
- `_merge_params` не сломан после переноса в отдельный модуль?
|
||||||
|
|
||||||
|
### 4. CSS/HTML разделение
|
||||||
|
- `render_template("index.html", ...)` передаёт все нужные переменные?
|
||||||
|
- CSS не потерян при переносе?
|
||||||
|
- `url_for('static', filename='style.css')` корректный?
|
||||||
|
|
||||||
|
### 5. Качество кода новых файлов
|
||||||
|
- Комментарии к каждой функции на месте?
|
||||||
|
- Имена переменных понятные?
|
||||||
|
- Нет дублирования логики между blueprint'ами?
|
||||||
|
|
||||||
|
## Формат ответа
|
||||||
|
|
||||||
|
Сгруппируй находки:
|
||||||
|
- 🔴 Критические (сломает работу)
|
||||||
|
- 🟡 Средние (потенциальная проблема)
|
||||||
|
- 🔵 Минорные (стиль, имена)
|
||||||
|
|
||||||
|
Для каждой: файл, проблема, предлагаемое исправление.
|
||||||
|
|
||||||
|
Если по какой-то категории всё ок — напиши «проблем не найдено».
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
# Задача: Code Review polygon v0.2.0
|
||||||
|
|
||||||
|
> Адресат: Claude Sonnet 4.6 (новый чат, с нуля)
|
||||||
|
> Дата: 2026-07-31
|
||||||
|
> ⛔ ОТВЕТ — ТОЛЬКО В ЧАТ. Не редактировать файлы. Не создавать файлы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что такое polygon
|
||||||
|
|
||||||
|
**Polygon** — эмулятор REST API облачной платформы Nubes. Отдельный managed-сервис
|
||||||
|
(`polygon.pythonk8s.dev.nubes.ru`), притворяется реальным Nubes API для интеграционных
|
||||||
|
тестов приложения **app-autotest**.
|
||||||
|
|
||||||
|
- Flask 3.0 + gunicorn, деплой на Nubes pythonk8s
|
||||||
|
- Все данные в памяти (MockState), без БД
|
||||||
|
- Data-driven: конфиги сервисов генерируются из STANDS YAML через `from_stands.py`
|
||||||
|
- 37 сервисов (dummy, postgres, redis, kafka, flask, ...)
|
||||||
|
- 17 API-эндпоинтов, префикс `/api/v1/svc`
|
||||||
|
- 19 юнит-тестов (все PASS)
|
||||||
|
|
||||||
|
Репозиторий: `https://gitea.services.ngcloud.ru/forcloud/polygon.git` (ветка `master`)
|
||||||
|
Деплой: `https://polygon.pythonk8s.dev.nubes.ru/`
|
||||||
|
Версия: **v0.2.0**
|
||||||
|
|
||||||
|
## 2. Структура кода
|
||||||
|
|
||||||
|
```
|
||||||
|
polygon/
|
||||||
|
├── requirements.txt # Flask>=3.0, gunicorn>=21.2, PyYAML>=6.0
|
||||||
|
├── .gitignore
|
||||||
|
├── tests/
|
||||||
|
│ ├── test_converter.py # 10 юнит-тестов from_stands.py
|
||||||
|
│ └── test_state_machine.py # 9 юнит-тестов state_machine.py + mock_state
|
||||||
|
└── site/
|
||||||
|
├── app.py # Flask-приложение (17 эндпоинтов)
|
||||||
|
├── mock_state.py # MockState: instances, operations, op_params в памяти
|
||||||
|
├── state_machine.py # apply_effect() — мутация состояния по kind/action
|
||||||
|
├── config_loader.py # загрузка services/*.yaml → {svcId: def} + {opId: def}
|
||||||
|
├── from_stands.py # конвертер STANDS YAML → polygon config
|
||||||
|
└── services/ # 37 сгенерированных YAML-конфигов
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Что делает каждый модуль
|
||||||
|
|
||||||
|
### app.py — 17 эндпоинтов
|
||||||
|
|
||||||
|
Полный эмулятор Nubes API. Порядок маршрутов критичен: `default/<int>` ДО `<uid>`.
|
||||||
|
|
||||||
|
| # | Метод | Путь | Назначение |
|
||||||
|
|---|-------|------|------------|
|
||||||
|
| 1 | GET | `/health` | `"OK"` (для Nubes healthcheck) |
|
||||||
|
| 2 | GET | `/` | HTML с версией, счётчиками |
|
||||||
|
| 3 | GET | `/api/v1/svc/services` | список сервисов |
|
||||||
|
| 4 | GET | `/api/v1/svc/services/<id>` | операции сервиса |
|
||||||
|
| 5 | GET | `/api/v1/svc/instances` | пагинация |
|
||||||
|
| 6 | GET | `/api/v1/svc/instances/<uid>` | инстанс + state.params + state.out |
|
||||||
|
| 7 | POST | `/api/v1/svc/instances` | создать shell → 201 + `Location: ./{uid}` |
|
||||||
|
| 8 | GET | `/api/v1/svc/instanceOperations/default/<id>` | cfsParams с dataDescriptor, valueList |
|
||||||
|
| 9 | POST | `/api/v1/svc/instanceOperations` | создать операцию → 201 + Location |
|
||||||
|
| 10 | GET | `/api/v1/svc/instanceOperations/<uid>` | статус + cfsParams (?fields=...) |
|
||||||
|
| 11 | POST | `/api/v1/svc/instanceOperationCfsParams` | paramId → value |
|
||||||
|
| 12 | GET | `/api/v1/svc/instanceOperations/<uid>/validate-cfs` | **пустое тело**, 200 |
|
||||||
|
| 13 | POST | `/api/v1/svc/instanceOperations/<uid>/run` | sleep → apply_effect → dtFinish |
|
||||||
|
| 14 | POST | `/api/v1/svc/_mock/reset` | сброс |
|
||||||
|
| 15 | GET | `/api/v1/svc/_mock/state` | отладка |
|
||||||
|
| 16 | GET | `/api/v1/svc/_mock/services` | отладка |
|
||||||
|
| 17 | POST | `/api/v1/svc/_mock/delay/<s>` | MOCK_OP_DELAY |
|
||||||
|
|
||||||
|
**Критические точки:**
|
||||||
|
- `POST /instances` и `POST /instanceOperations` **обязаны** отдавать `Location: ./{uuid}` — app-autotest достаёт UUID из заголовка
|
||||||
|
- `POST /instanceOperations` при `operation=="create"` — тело НЕ содержит `svcOperationId`, polygon ищет сам
|
||||||
|
- `validate-cfs`: `return "", 200` (НЕ `jsonify`) — app-autotest ждёт пустое тело
|
||||||
|
- `run`: синхронный `time.sleep(MOCK_OP_DELAY)` + `apply_effect` + `dtFinish = now`
|
||||||
|
- `MOCK_OP_DELAY` из env, по умолчанию 0.1с
|
||||||
|
|
||||||
|
### mock_state.py — состояние в памяти
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MockState:
|
||||||
|
instances = {} # instanceUid → {instanceUid, serviceId, displayName, status, state: {params, out}, ...}
|
||||||
|
operations = {} # opUid → {instanceOperationUid, instanceUid, svcOperationId, operation, kind, action, dtStart, dtFinish, isSuccessful, ...}
|
||||||
|
op_params = {} # opUid → {paramId(int): paramValue(str)}
|
||||||
|
```
|
||||||
|
|
||||||
|
Методы: `create_instance`, `get_instance`, `list_instances` (пагинация), `create_operation`,
|
||||||
|
`get_operation`, `set_param`, `get_params`, `reset`.
|
||||||
|
|
||||||
|
UUID через `uuid.uuid4()`. Пагинация: pageSize ≤ 200, стоп по `len(batch) < pageSize`.
|
||||||
|
|
||||||
|
### state_machine.py — apply_effect
|
||||||
|
|
||||||
|
Мутирует MockState после завершения операции:
|
||||||
|
|
||||||
|
| kind | action | Эффект |
|
||||||
|
|------|--------|--------|
|
||||||
|
| instance | create | статус `running`, stateParams из шаблона, stateOut из шаблона |
|
||||||
|
| instance | modify | мерж op_params в state.params через cfsParamsByOp |
|
||||||
|
| instance | delete | удалить инстанс |
|
||||||
|
| instance | suspend | статус `suspended` |
|
||||||
|
| instance | resume | статус `running` |
|
||||||
|
| instance | redeploy | статус `running` |
|
||||||
|
| instance | restart/recovery/... | no-op |
|
||||||
|
| subresource | create | `state.out[plural][name] = {}` |
|
||||||
|
| subresource | delete | `del state.out[plural][name]` |
|
||||||
|
|
||||||
|
`_extract_subresource_name`: ищет первое непустое строковое значение в op_params, fallback на subresource_name.
|
||||||
|
|
||||||
|
### config_loader.py — загрузка конфигов
|
||||||
|
|
||||||
|
Читает все `.yaml` из `services/`. Возвращает:
|
||||||
|
- `services`: `{service_id: service_def}`
|
||||||
|
- `ops_index`: `{svcOperationId: service_def}` — для поиска сервиса по ID операции
|
||||||
|
|
||||||
|
### from_stands.py — конвертер
|
||||||
|
|
||||||
|
Конвертирует STANDS YAML (из Terraform-провайдера) → polygon-конфиг.
|
||||||
|
Запуск: `python from_stands.py <STANDS_DIR> [SERVICES_DIR]`
|
||||||
|
|
||||||
|
- `html.unescape()` для всех строк (`>` → `>`, `"` → `"`)
|
||||||
|
- Маппинг полей: `id`→`svcOperationCfsParamId`, `code`→`svcOperationCfsParam`, `data_type`→`dataType`, `value_list`→`valueList`, `sub_params`→`dataDescriptor`
|
||||||
|
- `dataDescriptor` генерируется для всех типов с `sub_params` (map, map-fixed, array-map-fixed)
|
||||||
|
- `state_out_template`: авто-генерация из операций с `kind=subresource, action=create`
|
||||||
|
- `stateParams`: defaults из create-операции, JSON-генерация для map-fixed
|
||||||
|
- `cfsParamsByOp`: связка opId → список paramId
|
||||||
|
|
||||||
|
## 4. Что проверять (фокус code review)
|
||||||
|
|
||||||
|
### Безопасность
|
||||||
|
- Все ли входные данные валидируются (JSON body, query params)?
|
||||||
|
- Есть ли возможность инъекции через `displayName`, `paramValue`?
|
||||||
|
- `_mock/*` эндпоинты — не утечка ли это на проде?
|
||||||
|
|
||||||
|
### Корректность API
|
||||||
|
- Совпадают ли форматы ответов с реальным Nubes API?
|
||||||
|
- Правильно ли обрабатываются краевые случаи: отсутствующий сервис, невалидный instanceUid, пустой body?
|
||||||
|
- Корректны ли статус-коды (201, 400, 404)?
|
||||||
|
- `Location`-заголовки правильного формата?
|
||||||
|
|
||||||
|
### Состояние и стейт-машина
|
||||||
|
- Нет ли гонок в MockState (хотя воркер один, но Flask debug mode reloads)?
|
||||||
|
- Все ли переходы стейт-машины корректны?
|
||||||
|
- Не теряются ли данные при modify/reset?
|
||||||
|
- Правильно ли работает пагинация при пустом/частичном списке?
|
||||||
|
|
||||||
|
### Конвертер
|
||||||
|
- Все ли краевые случаи STANDS YAML обрабатываются (пустые поля, отсутствующие sub_params)?
|
||||||
|
- Правильно ли `html.unescape` применяется ко всем строковым полям?
|
||||||
|
- Не падает ли на нестандартных YAML (template, s3bucket)?
|
||||||
|
|
||||||
|
### Код и архитектура
|
||||||
|
- Нет ли дублирования логики?
|
||||||
|
- Понятны ли имена функций/переменных?
|
||||||
|
- Нет ли мёртвого кода?
|
||||||
|
- Правильно ли обрабатываются ошибки (try/except где нужно)?
|
||||||
|
|
||||||
|
### Nubes-совместимость
|
||||||
|
- `site/__init__.py` отсутствует?
|
||||||
|
- `app.run(host="0.0.0.0", port=5000)` на месте?
|
||||||
|
- `from site.xxx` нигде нет?
|
||||||
|
- `/health` возвращает `"OK"`?
|
||||||
|
|
||||||
|
## 5. Формат ответа
|
||||||
|
|
||||||
|
Сгруппируй находки по категориям:
|
||||||
|
- 🔴 Критические (сломает работу)
|
||||||
|
- 🟡 Средние (потенциальная проблема)
|
||||||
|
- 🔵 Минорные (стиль, имена)
|
||||||
|
|
||||||
|
Для каждой находки: файл, строка (примерная), проблема, предлагаемое исправление.
|
||||||
|
|
||||||
|
Если код в порядке — скажи что всё ок по каждой категории.
|
||||||
|
|
||||||
|
## 6. Что можно спрашивать у меня
|
||||||
|
|
||||||
|
Можешь задавать уточняющие вопросы. Например:
|
||||||
|
- «Какой формат у real Nubes API для эндпоинта X?»
|
||||||
|
- «Почему сделано так, а не иначе?»
|
||||||
|
- «Какие именно поля ждёт app-autotest в ответе Y?»
|
||||||
|
|
||||||
|
Формат вопросов:
|
||||||
|
```
|
||||||
|
### Вопрос N: <краткий заголовок>
|
||||||
|
<развёрнутый вопрос>
|
||||||
|
```
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# Соннет: анализ сравнительного тестирования Polygon ↔ реальный Nubes API
|
||||||
|
|
||||||
|
> Адресат: Claude Sonnet 4.6 (новый чат)
|
||||||
|
> ⛔ Режим: **диалог**. Задавай встречные вопросы если нужно уточнение.
|
||||||
|
> ⛔ НЕ редактировать файлы. Только анализ и советы в чат.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
**Polygon** (v0.5.4) — эмулятор REST API облачной платформы Nubes.
|
||||||
|
3 стенда: dev (37 сервисов), test (37), prod (35). YAML-конфиги генерируются
|
||||||
|
из терраформ-репы (`~/tf_provider/generated/{dev,test,prod}/resources_yaml/`).
|
||||||
|
|
||||||
|
**Реальное API**:
|
||||||
|
- `https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc`
|
||||||
|
- `https://lk-api-gateway-test.ngcloud.ru/api/v1/svc`
|
||||||
|
- `https://lk-api-gateway.ngcloud.ru/api/v1/svc`
|
||||||
|
|
||||||
|
Токены в `secrets/{dev,test,prod}.token`.
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
Написан скрипт `compare_test.py` который сравнивает read-only эндпоинты
|
||||||
|
полигона и реального API:
|
||||||
|
- `GET /services` — список сервисов
|
||||||
|
- `GET /services/{id}` — операции (svcOperationId, operation, kind, action)
|
||||||
|
- `GET /instanceOperations/default/{id}` — cfsParams (id, код, dataType, isRequired)
|
||||||
|
|
||||||
|
Первый прогон показал:
|
||||||
|
- **dev**: операции совпадают, но у 2 параметров `dataType: None` вместо `"string"`
|
||||||
|
- **test**: аналогично
|
||||||
|
- **prod**: чисто, расхождений нет
|
||||||
|
|
||||||
|
Также обнаружено что реальный API возвращает HTML-entities в dataType
|
||||||
|
(`integer >= 0`), а полигон — чистый текст (`integer >= 0`).
|
||||||
|
Полигон здесь правильнее реального API.
|
||||||
|
|
||||||
|
## Ключевой нюанс: идеология стендов
|
||||||
|
|
||||||
|
Стенды НЕ идентичны. **Dev опережает test, test опережает prod**.
|
||||||
|
Новые сервисы и параметры появляются сначала в dev, потом через какое-то
|
||||||
|
время попадают в test, и только затем в prod. Поэтому:
|
||||||
|
|
||||||
|
- Если в dev-полигоне и dev-реальном API есть расхождения — это может быть
|
||||||
|
нормально (реальный API уже обновился, а YAML в полигоне — ещё нет)
|
||||||
|
- Если в prod есть расхождения — скорее всего баг в генерации YAML
|
||||||
|
- Нужно различать «допустимое отставание» и «реальный баг»
|
||||||
|
|
||||||
|
## Что нужно от тебя
|
||||||
|
|
||||||
|
### 1. Стратегия сравнительного тестирования
|
||||||
|
|
||||||
|
Как правильно сравнивать полигон с реальным API учитывая что:
|
||||||
|
- Стенды могут и должны отличаться
|
||||||
|
- YAML генерируется не в реальном времени, а батчами из терраформа
|
||||||
|
- Некоторые сервисы есть в реальном API но НЕ в терраформе (их не тестируем)
|
||||||
|
|
||||||
|
Что должно считаться PASS, а что FAIL? Какие допуски?
|
||||||
|
|
||||||
|
### 2. Какие ещё эндпоинты сравнивать?
|
||||||
|
|
||||||
|
Сейчас сравниваются 3 read-only эндпоинта. Какие ещё можно безопасно
|
||||||
|
сравнять? Что ещё есть в реальном API такого что полигон должен
|
||||||
|
повторять один-в-один?
|
||||||
|
|
||||||
|
### 3. Периодичность и автоматизация
|
||||||
|
|
||||||
|
Как часто запускать сравнение? При каких событиях (изменение терраформа,
|
||||||
|
деплой полигона)? Должно ли это быть частью CI?
|
||||||
|
|
||||||
|
### 4. dataType: None
|
||||||
|
|
||||||
|
В `from_stands.py` для некоторых параметров dataType падает в None
|
||||||
|
(хотя дефолт "string"). Где конкретно искать причину?
|
||||||
|
|
||||||
|
### 5. Общие советы
|
||||||
|
|
||||||
|
Что ещё мы упускаем в тестировании полигона? Какие сценарии, краевые
|
||||||
|
случаи, проверки контрактов?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Формат диалога
|
||||||
|
|
||||||
|
Ты можешь:
|
||||||
|
- Сразу дать развёрнутый ответ по всем пунктам
|
||||||
|
- Или задать уточняющие вопросы — и тогда я отвечу, а ты продолжишь
|
||||||
|
|
||||||
|
Я хочу чтобы в итоге получился **конкретный план действий**:
|
||||||
|
что тестировать, как часто, что считать ошибкой, что — допустимым
|
||||||
|
расхождением.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ответ Соннета (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. Замечания по коду/архитектуре
|
||||||
|
|
||||||
|
Что можно улучшить не переписывая всё? Любые баги, уязвимости,
|
||||||
|
потенциальные проблемы которые ты видишь.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Формат: свободный. Главное — **конкретные советы** с указанием что и где
|
||||||
|
менять, а не общие рассуждения.
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Консультация: интеграция polygon ↔ app-autotest
|
||||||
|
|
||||||
|
> Адресат: Claude Sonnet 4.6 (новый чат)
|
||||||
|
> Дата: 2026-07-31
|
||||||
|
> ⛔ ОТВЕТ — ТОЛЬКО В ЧАТ. Не редактировать файлы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
**Polygon** (v0.2.4) — эмулятор Nubes API. Задеплоен на `polygon.pythonk8s.dev.nubes.ru`,
|
||||||
|
17 эндпоинтов с префиксом `/api/v1/svc`, 37 сервисов, все в памяти.
|
||||||
|
Принимает те же запросы что реальный Nubes API: POST /instances → 201 + Location,
|
||||||
|
POST /instanceOperations → 201 + Location, GET /instanceOperations/default/{id} → cfsParams,
|
||||||
|
POST /run → sleep + apply_effect + dtFinish.
|
||||||
|
|
||||||
|
**app-autotest** (v1.2.23) — Flask-приложение для ручного/сценарного тестирования Nubes.
|
||||||
|
Делает реальные HTTP-запросы к Nubes API через `HttpClient` (обёртка над `requests.Session`).
|
||||||
|
Сейчас ходит только в реальные dev/test стенды.
|
||||||
|
|
||||||
|
**Задача:** сделать так чтобы app-autotest мог ходить в polygon вместо реального API.
|
||||||
|
|
||||||
|
## Как app-autotest подключается к API (сейчас)
|
||||||
|
|
||||||
|
Файл `api/auth.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_client():
|
||||||
|
token = get_token()
|
||||||
|
endpoint = detect_endpoint(token) or current_app.config["NUBES_API_ENDPOINT"]
|
||||||
|
return HttpClient(endpoint, token)
|
||||||
|
|
||||||
|
def get_stand():
|
||||||
|
token = get_token()
|
||||||
|
endpoint = detect_endpoint(token) or current_app.config["NUBES_API_ENDPOINT"]
|
||||||
|
return stand_name(endpoint)
|
||||||
|
```
|
||||||
|
|
||||||
|
`detect_endpoint(token)` — пробует dev/test стенды, возвращает URL первого ответившего.
|
||||||
|
Хардкод: `["https://lk-api-gateway-dev...", "https://lk-api-gateway-test..."]`.
|
||||||
|
|
||||||
|
`stand_name(endpoint)` — "dev" или "test" по URL.
|
||||||
|
|
||||||
|
`HttpClient(endpoint, token)` — обёртка `requests.Session`:
|
||||||
|
- `get(path)` — GET, `.json()`
|
||||||
|
- `post(path, body)` — POST, извлекает UUID из заголовка `Location`
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
`detect_endpoint(token)` ВСЕГДА находит реальный стенд (токен валиден).
|
||||||
|
До fallback на `NUBES_API_ENDPOINT` дело НЕ доходит.
|
||||||
|
Даже при `NUBES_API_ENDPOINT=http://localhost:5000` app-autotest долбится в облако.
|
||||||
|
|
||||||
|
## Предлагаемое решение
|
||||||
|
|
||||||
|
Добавить проверку localhost в `get_client()` и `get_stand()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _is_localhost(url):
|
||||||
|
return (url or "").startswith(("http://localhost", "http://127.0.0.1"))
|
||||||
|
|
||||||
|
def get_client():
|
||||||
|
token = get_token()
|
||||||
|
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
||||||
|
if not _is_localhost(endpoint):
|
||||||
|
endpoint = detect_endpoint(token) or endpoint
|
||||||
|
return HttpClient(endpoint, token)
|
||||||
|
|
||||||
|
def get_stand():
|
||||||
|
token = get_token()
|
||||||
|
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
||||||
|
if _is_localhost(endpoint):
|
||||||
|
return "mock"
|
||||||
|
endpoint = detect_endpoint(token) or endpoint
|
||||||
|
return stand_name(endpoint)
|
||||||
|
```
|
||||||
|
|
||||||
|
Логика:
|
||||||
|
- Если `NUBES_API_ENDPOINT` указывает на localhost → пропустить detect_endpoint, сразу использовать его
|
||||||
|
- `get_stand()` для localhost → "mock" (вместо "?" которое вернёт stand_name для неизвестного URL)
|
||||||
|
- Ноль новых env-переменных, `NUBES_API_ENDPOINT` уже есть в конфиге
|
||||||
|
|
||||||
|
## Вопросы к Соннету
|
||||||
|
|
||||||
|
### Q1. Достаточно ли проверки localhost?
|
||||||
|
|
||||||
|
Сейчас: `startswith(("http://localhost", "http://127.0.0.1"))`.
|
||||||
|
|
||||||
|
Что насчёт:
|
||||||
|
- `https://polygon.pythonk8s.dev.nubes.ru` — удалённый polygon, НЕ localhost. `detect_endpoint` попытается dev/test и упадёт (потому что polygon не вернёт реальный токен-челлендж). Нужна ли более широкая проверка — например «любой не-deck URL»?
|
||||||
|
- IPv6 localhost `[::1]`?
|
||||||
|
- Docker-сети `http://host.docker.internal`?
|
||||||
|
|
||||||
|
### Q2. stand_name для polygon
|
||||||
|
|
||||||
|
Сейчас `stand_name("http://localhost:5000")` вернёт `"?"` потому что там нет "dev"/"test".
|
||||||
|
Мы предлагаем "mock" для localhost. Но что насчёт:
|
||||||
|
- `https://polygon.pythonk8s.dev.nubes.ru` — stand_name вернёт `"?"` потому что в URL нет "dev"/"test". Нужно ли добавить "polygon" в stand_name?
|
||||||
|
- Трекер инстансов (`/tmp/instances-{clientId}-{stand}.json`) использует stand для имени файла. "?" или "mock" — ок, но для удалённого polygon тоже будет "?".
|
||||||
|
|
||||||
|
### Q3. HttpClient — нужны ли изменения?
|
||||||
|
|
||||||
|
Polygon отдаёт ровно те же форматы что реальный API:
|
||||||
|
- `Location: ./{uuid}` на POST /instances и POST /instanceOperations
|
||||||
|
- `{"results": [...]}` на GET /instances
|
||||||
|
- `{"svcOperation": {"cfsParams": [...]}}` на GET /instanceOperations/default/{id}
|
||||||
|
- Пустое тело 200 на validate-cfs
|
||||||
|
|
||||||
|
Нужно ли что-то менять в `HttpClient.post()` или `http_client.py`?
|
||||||
|
|
||||||
|
### Q4. Токен для polygon
|
||||||
|
|
||||||
|
Polygon НЕ проверяет токен (все `_mock/*` открыты, основные эндпоинты тоже без auth).
|
||||||
|
app-autotest ВСЕГДА шлёт `Authorization: Bearer <token>` через HttpClient.
|
||||||
|
|
||||||
|
Это ок? Или polygon должен проверять токен (хотя бы непустой)?
|
||||||
|
|
||||||
|
### Q5. Порядок запуска для тестов
|
||||||
|
|
||||||
|
При локальном тестировании:
|
||||||
|
```bash
|
||||||
|
# Терминал 1: polygon
|
||||||
|
cd polygon/site && python app.py # порт 5000
|
||||||
|
|
||||||
|
# Терминал 2: app-autotest → polygon
|
||||||
|
cd app-autotest/site && \
|
||||||
|
NUBES_API_ENDPOINT=http://localhost:5000/api/v1/svc \
|
||||||
|
python app.py # порт 5001 или другой
|
||||||
|
```
|
||||||
|
|
||||||
|
Нет ли коллизий портов? app-autotest по умолчанию на 5000, polygon тоже на 5000.
|
||||||
|
|
||||||
|
### Q6. Альтернативный подход
|
||||||
|
|
||||||
|
Вместо проверки localhost, может лучше:
|
||||||
|
- Новая env-переменная `NUBES_MOCK=1` — явное переключение в режим мока?
|
||||||
|
- Или `NUBES_API_ENDPOINT` как единственный источник, а detect_endpoint вызывать только если endpoint НЕ задан явно?
|
||||||
|
|
||||||
|
Какой подход надёжнее?
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
# Задача: спроектировать мок-полигон Nubes API
|
||||||
|
|
||||||
|
> Адресат: Claude Sonnet 4.6
|
||||||
|
> Дата: 2026-07-31
|
||||||
|
> ⛔ ОТВЕТ — ТОЛЬКО В ЧАТ. Не редактировать файлы. Не создавать файлы. Только текст в чат.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что такое polygon
|
||||||
|
|
||||||
|
**Polygon** — отдельный managed-сервис на Nubes (`polygon.pythonk8s.dev.nubes.ru`),
|
||||||
|
эмулирующий REST API облачной платформы Nubes. Нужен для интеграционных тестов
|
||||||
|
приложения **app-autotest** — чтобы тесты гонялись не на реальном облаке, а на
|
||||||
|
локальном/CI эмуляторе.
|
||||||
|
|
||||||
|
**Ключевое:** polygon — это НЕ часть app-autotest. Это самостоятельный сервис
|
||||||
|
со своим репозиторием (`https://gitea.services.ngcloud.ru/forcloud/polygon.git`),
|
||||||
|
своим деплоем, своей версией (v0.1.0).
|
||||||
|
|
||||||
|
### Текущее состояние
|
||||||
|
|
||||||
|
Сервис запущен на Nubes, код минимальный:
|
||||||
|
|
||||||
|
```
|
||||||
|
polygon/
|
||||||
|
├── requirements.txt # Flask>=3.0, gunicorn>=21.2, PyYAML>=6.0
|
||||||
|
├── README.md
|
||||||
|
├── .gitignore
|
||||||
|
└── site/
|
||||||
|
└── app.py # 4 эндпоинта: /health, /, /api/v1/svc/instances,
|
||||||
|
# /api/v1/svc/_mock/reset
|
||||||
|
```
|
||||||
|
|
||||||
|
`site/app.py` — стандартный Flask по инструкции Nubes:
|
||||||
|
- `app = Flask(__name__, template_folder="templates", static_folder="static")`
|
||||||
|
- `/health` → `"OK"` (healthcheck)
|
||||||
|
- `/` → HTML-страница с версией
|
||||||
|
- `app.run(debug=True, host="0.0.0.0", port=5000)` в `if __name__ == "__main__"`
|
||||||
|
|
||||||
|
Gunicorn на проде: `gunicorn app:app --workers 2 --bind 0.0.0.0:8000`.
|
||||||
|
|
||||||
|
## 2. Исходные данные: STANDS YAML
|
||||||
|
|
||||||
|
В `STANDS/test/resources_yaml/` лежат **37 YAML-файлов** — описания ВСЕХ сервисов
|
||||||
|
Nubes в формате Terraform-провайдера. Это КАНОНИЧЕСКИЙ источник данных о сервисах:
|
||||||
|
их параметры, операции, типы данных, дефолты, valueList'ы.
|
||||||
|
|
||||||
|
### Структура STANDS YAML (на примере dummy и postgres)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 1_dummy.yaml (простой сервис, 339 строк)
|
||||||
|
name: dummy
|
||||||
|
service_id: 1
|
||||||
|
service_display_name: Болванка
|
||||||
|
service_short_name: dummy
|
||||||
|
lifecycle:
|
||||||
|
suspend_on_destroy_default: true
|
||||||
|
adopt_existing_on_create_default: false
|
||||||
|
outputs:
|
||||||
|
params:
|
||||||
|
- code: state_params # map
|
||||||
|
- code: state_out # map
|
||||||
|
- code: vault_secrets # map, sensitive
|
||||||
|
# ... ещё 5 outputs
|
||||||
|
operations:
|
||||||
|
- name: create
|
||||||
|
id: 18
|
||||||
|
kind: instance
|
||||||
|
action: create
|
||||||
|
params:
|
||||||
|
- id: 242
|
||||||
|
code: resourceRealm
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: dummy
|
||||||
|
value_list: [dummy]
|
||||||
|
- id: 198
|
||||||
|
code: durationMs
|
||||||
|
data_type: "integer >= 0"
|
||||||
|
required: true
|
||||||
|
default: "0"
|
||||||
|
- id: 199
|
||||||
|
code: failAtStart
|
||||||
|
data_type: boolean
|
||||||
|
required: true
|
||||||
|
default: "false"
|
||||||
|
value_list: ["false", "true"]
|
||||||
|
- id: 321
|
||||||
|
code: someMapParam
|
||||||
|
data_type: map
|
||||||
|
sub_params:
|
||||||
|
- id: 322
|
||||||
|
code: subparam1
|
||||||
|
data_type: string
|
||||||
|
- id: 323
|
||||||
|
code: secret
|
||||||
|
data_type: string
|
||||||
|
- name: modify
|
||||||
|
id: 92
|
||||||
|
kind: instance
|
||||||
|
action: modify
|
||||||
|
params: [...] # те же параметры, is_modifiable: true
|
||||||
|
- name: delete
|
||||||
|
id: 71
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 90_postgres.yaml (самый сложный, 934 строки)
|
||||||
|
operations:
|
||||||
|
- name: create # id: 19, kind: instance
|
||||||
|
- name: delete # id: 20
|
||||||
|
- name: modify # id: 40
|
||||||
|
- name: suspend # id: 41
|
||||||
|
- name: resume # id: 42
|
||||||
|
- name: restart # id: 43 ← лишняя?
|
||||||
|
- name: recovery # id: 24 ← лишняя?
|
||||||
|
- name: create_user # id: 44 ← subresource!
|
||||||
|
- name: delete_user # id: 45 ← subresource!
|
||||||
|
- name: create_database # id: 46 ← subresource!
|
||||||
|
- name: delete_database # id: 47 ← subresource!
|
||||||
|
|
||||||
|
# У create-операции — map-fixed с sub_params:
|
||||||
|
- name: create
|
||||||
|
params:
|
||||||
|
- id: 788
|
||||||
|
code: clusterConfiguration
|
||||||
|
data_type: map-fixed
|
||||||
|
sub_params:
|
||||||
|
- id: 106
|
||||||
|
code: replicas
|
||||||
|
data_type: "integer > 0"
|
||||||
|
default: "1"
|
||||||
|
value_list: ["1", "3", "5", "7"]
|
||||||
|
- id: 107
|
||||||
|
code: disk
|
||||||
|
data_type: "integer > 0"
|
||||||
|
default: "10"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Спектр сложности (37 сервисов)
|
||||||
|
|
||||||
|
| Тип | Примеры | Особенности |
|
||||||
|
|-----|---------|-------------|
|
||||||
|
| Простые | dummy, s3, http, gitea, nodejs, flask | Плоские параметры, 6 операций |
|
||||||
|
| Средние | redis, mongodb, rabbitmq, clickhouse, kafka | map-fixed (cluster/access/startup) |
|
||||||
|
| Сложные | postgres, mariadb | + subresources (create_user, create_database) |
|
||||||
|
| Инфраструктурные | vc_org, vc_vdc, vc_nsxt, vapp, vc_vm_v2/v3 | Другие kind'ы, refSvcId на другие сервисы |
|
||||||
|
| Особые | s3bucket (refSvcId на s3), template, k8s_velero, llm_ai | Нестандартные параметры |
|
||||||
|
|
||||||
|
Все 37 файлов лежат здесь: `STANDS/test/resources_yaml/`
|
||||||
|
Полный список: `1_dummy.yaml`, `2_template.yaml`, `12_s3.yaml`, `13_s3bucket.yaml`,
|
||||||
|
`19_vc_org.yaml`, `21_vc_vdc.yaml`, `22_vc_nsxt.yaml`, `25_vcexternalip.yaml`,
|
||||||
|
`26_vapp.yaml`, `27_vc_vm_v2.yaml`, `28_vc_vm_v3.yaml`, `29_vc_vdc_group.yaml`,
|
||||||
|
`50_nextcloud.yaml`, `81_superset.yaml`, `82_harbor.yaml`, `86_k8s_velero.yaml`,
|
||||||
|
`89_flask.yaml`, `90_postgres.yaml`, `91_redis.yaml`, `92_mongodb.yaml`,
|
||||||
|
`93_rabbitmq.yaml`, `94_lucee.yaml`, `95_nodejs.yaml`, `96_pgadmin.yaml`,
|
||||||
|
`98_http.yaml`, `99_gitea.yaml`, `109_zones_v2.yaml`, `111_dnsrecord.yaml`,
|
||||||
|
`115_mariadb.yaml`, `116_kafka.yaml`, `119_akhq.yaml`, `120_clickhouse.yaml`,
|
||||||
|
`148_vc_mgmt_sthutrval_cluster.yaml`, `149_valo_tenant.yaml`,
|
||||||
|
`150_k8s_sthutrval_cluster.yaml`, `151_k8s_openbao.yaml`, `163_llm_ai.yaml`.
|
||||||
|
|
||||||
|
## 3. Архитектурные решения (уже приняты)
|
||||||
|
|
||||||
|
Предыдущий архитектор (Опус) принял 10 решений. Их нужно УЧЕСТЬ, но можно
|
||||||
|
ОСПОРИТЬ если найдёшь лучший подход.
|
||||||
|
|
||||||
|
| # | Решение | Обоснование |
|
||||||
|
|---|---------|-------------|
|
||||||
|
| 1 | Отдельный процесс :5000 | `http_client` делает реальные HTTP-запросы — blueprint не проверит |
|
||||||
|
| 2 | Data-driven: сервисы из YAML | Не хардкодить 37 сервисов в Python |
|
||||||
|
| 3 | Мин. поля + `default_for(dataType)` | 20+ параметров вручную — ад |
|
||||||
|
| 4 | Ленивый dtFinish | Без потоков, `MOCK_OP_DELAY` (по умолчанию 0.1с) |
|
||||||
|
| 5 | Единая стейт-машина | create→running→suspended→deleted |
|
||||||
|
| 6 | Реальный мерж params при modify | Иначе тест modify→проверить state.params бессмысленен |
|
||||||
|
| 7 | Статический stateOut из YAML | Для MVP, генерация из параметров — потом |
|
||||||
|
| 8 | `/_mock/reset` для тестов | Без сброса тесты влияют друг на друга |
|
||||||
|
| 9 | Тесты через `app_client` | Проверяет реальную связку app-autotest ↔ эмулятор |
|
||||||
|
| 10 | refSvcId игнорируем в MVP | validate-cfs всегда OK, ссылки не проверяются |
|
||||||
|
|
||||||
|
## 4. Критические точки интеграции (из кода app-autotest)
|
||||||
|
|
||||||
|
Это НЕ предположения — это факты из реального кода, который будет ходить в polygon:
|
||||||
|
|
||||||
|
### 4.1 Location-заголовки обязательны
|
||||||
|
|
||||||
|
`HttpClient.post()` достаёт UUID из заголовка `Location` (последний сегмент, `len >= 32`).
|
||||||
|
Polygon ОБЯЗАН отдавать:
|
||||||
|
- `POST /api/v1/svc/instances` → `201` + `Location: ./{instanceUid}`
|
||||||
|
- `POST /api/v1/svc/instanceOperations` → `201` + `Location: ./{instanceOperationUid}`
|
||||||
|
|
||||||
|
Без Location executor не получит UUID → ошибка.
|
||||||
|
|
||||||
|
### 4.2 Поллинг
|
||||||
|
|
||||||
|
`poll_until_done()` делает `GET /instanceOperations/{uid}?fields=...`,
|
||||||
|
ждёт `dtFinish`, спит 5 секунд между попытками. При `MOCK_OP_DELAY=0.1`
|
||||||
|
dtFinish появится на первом же GET — вторая итерация со сном не случится.
|
||||||
|
|
||||||
|
### 4.3 Short-circuit localhost в auth.py
|
||||||
|
|
||||||
|
`detect_endpoint()` в app-autotest хардкодит dev/test стенды и игнорирует
|
||||||
|
`NUBES_API_ENDPOINT`. Нужно добавить проверку: если endpoint начинается с
|
||||||
|
`http://localhost` или `http://127.0.0.1` → пропустить detect, сразу использовать.
|
||||||
|
Это будет сделано в app-autotest, не в polygon.
|
||||||
|
|
||||||
|
### 4.4 validate-cfs = пустое тело
|
||||||
|
|
||||||
|
`send_params_terraform()` считает успехом пустой/не-JSON ответ.
|
||||||
|
Polygon отдаёт `200` с пустым телом.
|
||||||
|
|
||||||
|
### 4.5 state.params по коду, cfsParams по числовому id
|
||||||
|
|
||||||
|
`get_params_with_current_values()` мержит `state.params[код]` с шаблоном из
|
||||||
|
`GET /instanceOperations/default/{opId}`.
|
||||||
|
YAML должен связывать числовой `svcOperationCfsParamId` ↔ код параметра.
|
||||||
|
|
||||||
|
### 4.6 Nubes-совместимость
|
||||||
|
|
||||||
|
Polygon деплоится как managed-сервис Nubes (pythonk8s). Следовательно:
|
||||||
|
- `site/app.py` — точка входа (платформа ждёт `python site/app.py`)
|
||||||
|
- `app.run(host="0.0.0.0", port=5000)` — обязательно
|
||||||
|
- ⛔ `site/__init__.py` — НЕЛЬЗЯ (конфликтует со stdlib `site.py`)
|
||||||
|
- ⛔ `from site.xxx import ...` — НЕЛЬЗЯ (импорт без префикса)
|
||||||
|
- `/health` → `"OK"` — healthcheck для Nubes
|
||||||
|
|
||||||
|
## 5. Что нужно от тебя
|
||||||
|
|
||||||
|
### 5.1 Архитектура проекта
|
||||||
|
|
||||||
|
Полная архитектура polygon с обоснованием каждого решения. Включая:
|
||||||
|
|
||||||
|
- **Структура файлов** в `site/` — какие модули, за что отвечают
|
||||||
|
- **Схема данных в памяти** — MockState: instances, operations, связи
|
||||||
|
- **Стейт-машина инстанса** — все статусы, переходы
|
||||||
|
- **Поток запроса** — что происходит от POST /instances до GET /instanceOperations/{uid}
|
||||||
|
- **Схема API** — полный список эндпоинтов с форматами запросов/ответов
|
||||||
|
|
||||||
|
### 5.2 Универсальный конвертер STANDS → polygon
|
||||||
|
|
||||||
|
Главный вопрос: **можно ли написать ОДИН конвертер для всех 37 сервисов без сервис-специфичного кода?**
|
||||||
|
|
||||||
|
Если да — спроектировать `from_stands.py`:
|
||||||
|
- Алгоритм конвертации одного YAML
|
||||||
|
- Маппинг ВСЕХ полей STANDS → polygon (таблица)
|
||||||
|
- Обработка краевых случаев (map-fixed, sub_params, subresources)
|
||||||
|
- Генерация stateParams из create-операции (defaults)
|
||||||
|
- Генерация stateOut (из операций create_user/create_database и т.п.)
|
||||||
|
- Выходной формат: отдельные файлы в `services/` или один dict?
|
||||||
|
|
||||||
|
Если нет — объяснить ГДЕ проходит граница и что придётся делать вручную.
|
||||||
|
|
||||||
|
### 5.3 Подробный план реализации
|
||||||
|
|
||||||
|
Пошаговый план с разбивкой на этапы. Для каждого этапа:
|
||||||
|
- Какие файлы создать/изменить
|
||||||
|
- Что именно реализовать (функции, классы, эндпоинты)
|
||||||
|
- Критерий готовности этапа
|
||||||
|
- Ожидаемые сложности
|
||||||
|
|
||||||
|
## 6. Конкретные вопросы
|
||||||
|
|
||||||
|
Ответь на каждый:
|
||||||
|
|
||||||
|
### Q1. from_stands.py
|
||||||
|
Как обрабатывать `map-fixed` параметры (clusterConfiguration, startupConfiguration)?
|
||||||
|
В STANDS: `params[].sub_params[]`. В реальном API: `dataDescriptor`.
|
||||||
|
Должен ли конвертер генерировать `dataDescriptor` из `sub_params`? Или моку
|
||||||
|
достаточно знать структуру чтобы принимать такие параметры при create?
|
||||||
|
|
||||||
|
### Q2. subresources
|
||||||
|
У postgres/mariadb есть `create_user`, `delete_user`, `create_database`, `delete_database`.
|
||||||
|
Можно ли вывести **общее правило**: «если операция имеет kind ≠ instance — это subresource»?
|
||||||
|
Или «если действие create/delete а операция не create/delete инстанса — subresource»?
|
||||||
|
|
||||||
|
Как универсально эмулировать subresource-операции? Сейчас идея: `apply_effect` смотрит на
|
||||||
|
`action` и `kind` — если `kind != "instance"`, значит меняем `state.out`, не `state.params`.
|
||||||
|
|
||||||
|
### Q3. stateOut
|
||||||
|
В реальном API после create PostgreSQL: `state.out = {users: {pgadmin: {}}, databases: {mydb: {}}}`.
|
||||||
|
Может ли конвертер автоматически вывести структуру stateOut из наличия subresource-операций?
|
||||||
|
Например: есть `create_user` → добавить `users: {}` в stateOut.
|
||||||
|
|
||||||
|
### Q4. Лишние операции
|
||||||
|
У postgres есть restart, recovery. У некоторых сервисов есть специфичные операции.
|
||||||
|
Включать их в мок? Если да — как эмулировать? Если нет — что отвечать на запрос
|
||||||
|
`GET /instanceOperations/default/{id}` для несуществующей операции?
|
||||||
|
|
||||||
|
### Q5. valueList
|
||||||
|
В STANDS YAML `value_list` есть у многих параметров. Нужно ли моку хранить их
|
||||||
|
и отдавать в `GET /instanceOperations/default/{id}` (cfsParams)? Или мок может
|
||||||
|
отдавать пустой `valueList` для всех?
|
||||||
|
|
||||||
|
### Q6. Config loader vs генерация
|
||||||
|
Два подхода:
|
||||||
|
- **A)** `from_stands.py` генерит YAML в `services/`, `config_loader.py` их читает при старте
|
||||||
|
- **B)** `from_stands.py` сразу возвращает dict, без промежуточных YAML
|
||||||
|
|
||||||
|
Какой лучше? Плюсы A: можно редактировать сгенерированные YAML руками для сложных сервисов.
|
||||||
|
Плюсы B: проще, меньше файлов.
|
||||||
|
|
||||||
|
### Q7. mock-специфичные эндпоинты
|
||||||
|
`/_mock/reset` — сброс состояния. Нужны ли ещё?
|
||||||
|
- `/_mock/state` — посмотреть текущее состояние (для отладки)?
|
||||||
|
- `/_mock/delay/{seconds}` — изменить MOCK_OP_DELAY на лету?
|
||||||
|
- `/_mock/services` — список загруженных сервисов?
|
||||||
|
|
||||||
|
### Q8. Идентификаторы
|
||||||
|
UUID для instanceUid и instanceOperationUid — генерить через `uuid.uuid4()`?
|
||||||
|
Или детерминированно (например, `uuid5(namespace, service_id + counter)`)?
|
||||||
|
|
||||||
|
### Q9. Пагинация
|
||||||
|
`GET /instances?pageSize=N&page=P` — как правильно эмулировать?
|
||||||
|
Стоп по `len(batch) < pageSize`? Что если pageSize > 200?
|
||||||
|
|
||||||
|
### Q10. Тестирование
|
||||||
|
5 сценариев из плана Опуса — достаточны? Какие ещё сценарии добавить?
|
||||||
|
Как организовать тесты чтобы они не зависели друг от друга?
|
||||||
|
|
||||||
|
## 7. Что можно спрашивать у меня
|
||||||
|
|
||||||
|
Ты можешь задавать мне уточняющие вопросы в текстовом виде. Я отвечу.
|
||||||
|
|
||||||
|
Что можно спрашивать:
|
||||||
|
- Уточнения по STANDS YAML (структура конкретных сервисов, поля которые не видел)
|
||||||
|
- Детали работы app-autotest (какие запросы шлёт, какие форматы ждёт)
|
||||||
|
- Приоритеты (что важнее сделать сначала)
|
||||||
|
- Ограничения Nubes-платформы
|
||||||
|
|
||||||
|
Что НЕ надо спрашивать:
|
||||||
|
- «Какие технологии использовать?» — только Flask, Python, PyYAML
|
||||||
|
- «Может использовать БД?» — нет, всё в памяти
|
||||||
|
- «Может Kubernetes API?» — нет, это чистый HTTP-эмулятор
|
||||||
|
|
||||||
|
Формат вопросов:
|
||||||
|
```
|
||||||
|
### Вопрос 1: <краткий заголовок>
|
||||||
|
<развёрнутый вопрос с контекстом>
|
||||||
|
```
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# Ответ Claude Sonnet 4.6 — архитектура и план polygon
|
||||||
|
|
||||||
|
Дата: 2026-07-31
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ответы на Q1–Q10
|
||||||
|
|
||||||
|
**Q1: map-fixed → dataDescriptor**
|
||||||
|
Конвертер ГЕНЕРИРУЕТ `dataDescriptor` из `sub_params`. Формат каждой записи:
|
||||||
|
```
|
||||||
|
{sub_param_code: {dataType, defaultValue, valueList, isRequired}}
|
||||||
|
```
|
||||||
|
Это обязательно: `normalize_value` в app-autotest использует `dataDescriptor` для генерации JSON из defaults при пустом значении map-параметра.
|
||||||
|
|
||||||
|
**Q2: subresources**
|
||||||
|
Правило простое и точное: `kind == "subresource"` (это явное поле в STANDS YAML).
|
||||||
|
`apply_effect` смотрит на `kind`:
|
||||||
|
- `kind == "instance"` → меняем статус + `state.params`
|
||||||
|
- `kind == "subresource"` → меняем `state.out[plural]`, статус НЕ меняем
|
||||||
|
|
||||||
|
**Q3: stateOut auto-generation**
|
||||||
|
Да, конвертер выводит автоматически. Сканирует операции с `kind=subresource, action=create`, строит `state_out_template`. Пример для postgres: `{users: {}, databases: {}, backups: {}}`. При create инстанса мок копирует этот шаблон в `instance.state.out`.
|
||||||
|
|
||||||
|
**Q4: "лишние" операции**
|
||||||
|
Включить все (restart, recovery, etc.) — иначе `GET /instanceOperations/default/{id}` вернёт 404 и app-autotest упадёт. Реализация: no-op transition (статус остаётся `running`, dtFinish ставится, isSuccessful=True). Если operation_id вообще не найден → 404 с `{"error": "operation not found"}`.
|
||||||
|
|
||||||
|
**Q5: valueList**
|
||||||
|
Хранить и отдавать обязательно. `get_params_with_current_values` в app-autotest заполняет из него дропдауны для `delete_user`/`delete_database`.
|
||||||
|
|
||||||
|
**Q6: Подход A (generate YAML → services/)**
|
||||||
|
Выбрать A. `from_stands.py` генерирует `site/services/N_name.yaml`, `config_loader.py` читает при старте Flask. Плюс: ручная правка для сложных случаев, прозрачность. `from_stands.py` запускается один раз при setup.
|
||||||
|
|
||||||
|
**Q7: Mock-эндпоинты**
|
||||||
|
Все 4:
|
||||||
|
- `POST /_mock/reset` — обязателен для тестовой изоляции
|
||||||
|
- `GET /_mock/state` — текущее состояние (отладка)
|
||||||
|
- `GET /_mock/services` — список загруженных сервисов
|
||||||
|
- `POST /_mock/delay/<float>` — изменить MOCK_OP_DELAY на лету
|
||||||
|
|
||||||
|
**Q8: UUID** → `uuid.uuid4()`. Детерминированные не нужны.
|
||||||
|
|
||||||
|
**Q9: Пагинация** → stop when `len(batch) < pageSize`, cap at 200, page=1 by default. Response: `{"results": [...], "pageSize": N, "page": P, "total": len(all)}`.
|
||||||
|
|
||||||
|
**Q10: Тесты**
|
||||||
|
К 5 сценариям Опуса добавить: (a) тест пагинации (3 инстанса, pageSize=2), (b) тест subresource create→verify state.out→delete→verify removed, (c) тест modify→verify params merged, (d) тест /_mock/state, (e) тест 404 на невалидный op_id. Каждый тест начинается с `POST /_mock/reset` (через `autouse` fixture).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Структура файлов
|
||||||
|
|
||||||
|
```
|
||||||
|
polygon/
|
||||||
|
├── requirements.txt
|
||||||
|
└── site/
|
||||||
|
├── app.py # Flask app + все 15 маршрутов (один файл)
|
||||||
|
├── mock_state.py # MockState: instances, operations, op_params
|
||||||
|
├── state_machine.py # apply_effect()
|
||||||
|
├── config_loader.py # load_services() → services_dict + ops_index
|
||||||
|
├── from_stands.py # CLI-конвертер STANDS YAML → services/*.yaml
|
||||||
|
├── services/ # 37 YAML-файлов (generated)
|
||||||
|
└── templates/
|
||||||
|
└── index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ключевое ограничение Nubes:**
|
||||||
|
- `site/__init__.py` — ЗАПРЕЩЁН
|
||||||
|
- `from site.xxx import` — ЗАПРЕЩЁН
|
||||||
|
- В app.py: только `import mock_state`, `import config_loader`, `import state_machine` (работает т.к. Python добавляет директорию скрипта в `sys.path[0]`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Схема данных в памяти
|
||||||
|
|
||||||
|
```python
|
||||||
|
# mock_state.py — синглтон state = MockState()
|
||||||
|
class MockState:
|
||||||
|
instances = {} # instanceUid → instance_dict
|
||||||
|
operations = {} # opUid → operation_dict
|
||||||
|
op_params = {} # opUid → {paramId(int): paramValue(str)}
|
||||||
|
```
|
||||||
|
|
||||||
|
**instance_dict:** `{instanceUid, serviceId, displayName, descr, status, explainedStatus, svc, dtCreate, state: {params: {code: val}, out: {users: {}, databases: {}}}}`
|
||||||
|
|
||||||
|
**operation_dict:** `{instanceOperationUid, instanceUid, svcOperationId, operation, kind, action, subresource|None, dtStart, dtFinish|None, isSuccessful|None, errorLog, svc, stages}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Конвертер from_stands.py
|
||||||
|
|
||||||
|
**HTML entities:** STANDS YAML содержит `integer >= 0`, `"` и т.д. `from_stands.py` **обязан** применять `html.unescape()` ко всем строковым полям (`data_type`, `value_list` элементы, `default`).
|
||||||
|
|
||||||
|
**Маппинг STANDS → cfsParam формат:**
|
||||||
|
|
||||||
|
| STANDS | polygon/cfsParam |
|
||||||
|
|---|---|
|
||||||
|
| `id` | `svcOperationCfsParamId` |
|
||||||
|
| `code` | `svcOperationCfsParam` |
|
||||||
|
| `data_type` (unescape) | `dataType` |
|
||||||
|
| `required` | `isRequired` |
|
||||||
|
| `default` | `defaultValue` |
|
||||||
|
| `value_list` | `valueList` |
|
||||||
|
| `sub_params` (map-fixed) | → `dataDescriptor: {code: {dataType,defaultValue,valueList,isRequired}}` |
|
||||||
|
| `sub_params` (map) | → `sub_params` (хранить как есть для normalize_value) |
|
||||||
|
|
||||||
|
**stateOut auto-generation:**
|
||||||
|
```python
|
||||||
|
for op in operations:
|
||||||
|
if op.kind == "subresource" and op.action == "create":
|
||||||
|
plural = op.subresource + "s" # user→users, database→databases
|
||||||
|
state_out_template[plural] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Полный список API эндпоинтов (15 шт.)
|
||||||
|
|
||||||
|
| # | Метод | Путь | Назначение |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | GET | `/health` | `"OK"` |
|
||||||
|
| 2 | GET | `/` | HTML-страница |
|
||||||
|
| 3 | GET | `/api/v1/svc/instances` | Список инстансов (paged) |
|
||||||
|
| 4 | GET | `/api/v1/svc/instances/<uid>` | Детали инстанса |
|
||||||
|
| 5 | POST | `/api/v1/svc/instances` | Создать shell → 201 + Location |
|
||||||
|
| 6 | GET | `/api/v1/svc/instanceOperations/default/<int:op_id>` | Шаблон операции |
|
||||||
|
| 7 | POST | `/api/v1/svc/instanceOperations` | Создать операцию → 201 + Location |
|
||||||
|
| 8 | GET | `/api/v1/svc/instanceOperations/<uid>` | Детали операции + cfsParams |
|
||||||
|
| 9 | POST | `/api/v1/svc/instanceOperationCfsParams` | Задать значение параметра |
|
||||||
|
| 10 | GET | `/api/v1/svc/instanceOperations/<uid>/validate-cfs` | Валидация → 200 **пустое тело** |
|
||||||
|
| 11 | POST | `/api/v1/svc/instanceOperations/<uid>/run` | Выполнить операцию |
|
||||||
|
| 12 | POST | `/api/v1/svc/_mock/reset` | Сброс состояния |
|
||||||
|
| 13 | GET | `/api/v1/svc/_mock/state` | Debug: текущее состояние |
|
||||||
|
| 14 | GET | `/api/v1/svc/_mock/services` | Debug: загруженные сервисы |
|
||||||
|
| 15 | POST | `/api/v1/svc/_mock/delay/<float:s>` | Изменить MOCK_OP_DELAY |
|
||||||
|
|
||||||
|
**Критические детали:**
|
||||||
|
|
||||||
|
- **Эндпоинт 5** (`POST /instances`): возвращает `201` + заголовок `Location: ./{uid}`. `HttpClient.post()` берёт uid из последнего сегмента Location, проверяет `len >= 32`. Body: `{"instanceUid": uid}` (двойная защита).
|
||||||
|
|
||||||
|
- **Эндпоинт 7** (`POST /instanceOperations`): при `operation=="create"` тело НЕ содержит `svcOperationId` — polygon находит его сам из `svc_def.operations["create"]["id"]`.
|
||||||
|
|
||||||
|
- **Эндпоинт 10** (validate-cfs): `return "", 200` (НЕ `jsonify`). app-autotest ловит `JSONDecodeError` и считает это успехом.
|
||||||
|
|
||||||
|
- **Эндпоинт 11** (run): синхронный — `time.sleep(MOCK_OP_DELAY)`, затем `apply_effect`, затем `dtFinish = datetime.utcnow().isoformat() + "Z"`. Первый poll после run увидит dtFinish.
|
||||||
|
|
||||||
|
- **Маршруты Flask:** `/instanceOperations/default/<int:op_id>` должен стоять **выше** `/instanceOperations/<uid>` в app.py.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поток запроса (полный цикл create)
|
||||||
|
|
||||||
|
```
|
||||||
|
executor.py polygon
|
||||||
|
───────── ───────
|
||||||
|
POST /instances {serviceId,displayName}
|
||||||
|
→ создать instance shell (status="creating")
|
||||||
|
→ response 201 + Location: ./uid1
|
||||||
|
← instanceUid = uid1
|
||||||
|
|
||||||
|
POST /instanceOperations {instanceUid, operation:"create"}
|
||||||
|
→ найти create op_id из service_def
|
||||||
|
→ создать operation_dict (dtFinish=None)
|
||||||
|
→ response 201 + Location: ./uid2
|
||||||
|
← opUid = uid2
|
||||||
|
|
||||||
|
GET /instanceOperations/uid2?fields=cfsParams
|
||||||
|
→ response {"instanceOperation": {"cfsParams": [...]}}
|
||||||
|
cfsParams из service_def + пустые paramValue
|
||||||
|
|
||||||
|
POST /instanceOperationCfsParams × N → state.op_params[uid2][paramId] = val
|
||||||
|
GET /instanceOperations/uid2/validate-cfs → response "" 200
|
||||||
|
POST /instanceOperations/uid2/run
|
||||||
|
→ sleep(0.1s)
|
||||||
|
→ apply_effect: state.params←codes, status="running"
|
||||||
|
→ dtFinish = now
|
||||||
|
→ response {"ok": true}
|
||||||
|
|
||||||
|
GET /instanceOperations/uid2?fields=dtFinish,...
|
||||||
|
→ response {"instanceOperation": {"dtFinish": "2026-...", "isSuccessful": true, ...}}
|
||||||
|
← poll done: status="OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фазы реализации
|
||||||
|
|
||||||
|
**Этап 1 — Конвертер** (`from_stands.py`): html.unescape, convert_param рекурсивный, build_state_out_template, pluralize, CLI. Запустить: `cd polygon/site && python from_stands.py`. Критерий: 37 YAML в `site/services/`, у postgres `state_out_template: {users:{}, databases:{}, backups:{}}`.
|
||||||
|
|
||||||
|
**Этап 2 — MockState + config_loader + state_machine**: три отдельных модуля. Критерий: `python -c "import config_loader; s,i=config_loader.load_services('services'); print(len(s))"` → 37.
|
||||||
|
|
||||||
|
**Этап 3 — Flask API** в app.py: реализовать все 15 эндпоинтов в порядке от простых к сложным (см. таблицу). Критерий: `python -m py_compile app.py` без ошибок, `curl localhost:5000/health` → OK.
|
||||||
|
|
||||||
|
**Этап 4 — Тесты**:
|
||||||
|
- В polygon: `tests/test_converter.py`, `tests/test_state_machine.py` (юнит)
|
||||||
|
- В app-autotest: `tests/conftest.py` (+fixture `polygon_server` subprocess), `tests/test_polygon_integration.py` (интеграционные)
|
||||||
|
|
||||||
|
|
||||||
|
## Уточнения (второй раунд)
|
||||||
|
|
||||||
|
### Уточнение 1: `map` с `sub_params` → тоже `dataDescriptor`
|
||||||
|
|
||||||
|
Правило: **если у параметра есть `sub_params` — генерировать `dataDescriptor`**, независимо от того `map` это или `map-fixed`.
|
||||||
|
|
||||||
|
| `data_type` | `sub_params` | Действие |
|
||||||
|
|---|---|---|
|
||||||
|
| `map-fixed` | есть | генерировать `dataDescriptor` |
|
||||||
|
| `map` | есть | генерировать `dataDescriptor` |
|
||||||
|
| `array-map-fixed` | есть | генерировать `dataDescriptor` |
|
||||||
|
| любой | нет | `dataDescriptor: null` |
|
||||||
|
|
||||||
|
### Уточнение 2: `state.out` после create — пустые `{}`
|
||||||
|
|
||||||
|
Достаточно пустых `{}` из `state_out_template`. STANDS YAML не описывает runtime-дефолты (`pgadmin`, `mydb`) — это эффект реального Terraform, не мок.
|
||||||
|
|
||||||
|
### Уточнение 3: расположение тестов
|
||||||
|
|
||||||
|
```
|
||||||
|
app-autotest/
|
||||||
|
tests/
|
||||||
|
conftest.py # + fixture polygon_server (subprocess)
|
||||||
|
test_polygon_integration.py # интеграционные тесты
|
||||||
|
|
||||||
|
polygon/
|
||||||
|
tests/
|
||||||
|
test_converter.py # юнит: from_stands.py
|
||||||
|
test_state_machine.py # юнит: apply_effect
|
||||||
|
```
|
||||||
|
|
||||||
|
`polygon_server` fixture: subprocess → poll `/health` (timeout 5s) → после тестов `terminate()`.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Задача: улучшить Swagger/OpenAPI в Polygon
|
||||||
|
|
||||||
|
> Адресат: Claude Sonnet 4.6 (новый чат)
|
||||||
|
> ⛔ ОТВЕТ — ТОЛЬКО В ЧАТ. Не редактировать файлы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
**Polygon** (v0.4.0) — эмулятор REST API облачной платформы Nubes.
|
||||||
|
Задеплоен на `polygon.pythonk8s.dev.nubes.ru`. 17 эндпоинтов, 37 сервисов.
|
||||||
|
|
||||||
|
Недавно добавлен Swagger UI (`/swagger`) + OpenAPI 3.1.0 спека (`/api/v1/svc/openapi.json`).
|
||||||
|
Сделано наспех — нужно улучшить.
|
||||||
|
|
||||||
|
## Текущая реализация
|
||||||
|
|
||||||
|
- `site/routes/openapi.py` — динамическая OpenAPI 3.1.0 спека (~500 строк Python)
|
||||||
|
- `site/templates/swagger.html` — Swagger UI 5 c CDN (~120 строк HTML+JS)
|
||||||
|
- `site/routes/root.py` — роут `/swagger` → render_template
|
||||||
|
- Токен авторизации предзаполняется (`test-token-123`)
|
||||||
|
- Nubes-брендированный topbar (лого + версия)
|
||||||
|
|
||||||
|
## Что нужно от тебя
|
||||||
|
|
||||||
|
### 1. Анализ текущего состояния
|
||||||
|
|
||||||
|
Найди проблемы и недочёты:
|
||||||
|
- Где спека не соответствует реальному поведению API?
|
||||||
|
- Где схемы неполные или неточные?
|
||||||
|
- Где Swagger UI неудобен (лишние эндпоинты, плохие группировки, запутанная навигация)?
|
||||||
|
- Где есть баги (неправильные методы, статус-коды, форматы)?
|
||||||
|
|
||||||
|
### 2. Решения
|
||||||
|
|
||||||
|
Для каждой проблемы — конкретное исправление. Покажи:
|
||||||
|
- **Что** поменять (файл, строка, фрагмент кода)
|
||||||
|
- **Почему** это улучшит (пользовательский опыт, точность, простота)
|
||||||
|
- **Приоритет** (обязательно / желательно / косметика)
|
||||||
|
|
||||||
|
### 3. Best practices
|
||||||
|
|
||||||
|
Научи как правильно:
|
||||||
|
- Группировать эндпоинты (tags) чтобы было логично
|
||||||
|
- Описывать схемы чтобы они были полезны в «Try it out»
|
||||||
|
- Скрывать служебные эндпоинты (`_mock/*` — нужны только для тестов)
|
||||||
|
- Писать summary/description на русском, коротко и по делу
|
||||||
|
- Обрабатывать авторизацию в Swagger UI чтобы работало из коробки
|
||||||
|
|
||||||
|
## Что НЕ надо
|
||||||
|
|
||||||
|
- Не предлагать переписывать всё с нуля
|
||||||
|
- Не добавлять новые pip-зависимости (Flask-RESTX, flask-swagger-ui, etc.)
|
||||||
|
- Не усложнять — полигон это мок, не прод
|
||||||
|
- Не предлагать автогенерацию из кода через декораторы
|
||||||
|
|
||||||
|
## Формат ответа
|
||||||
|
|
||||||
|
Сгруппируй находки так:
|
||||||
|
|
||||||
|
### 🔴 Баги (не работает / неверно)
|
||||||
|
### 🟡 Удобство (путает, неудобно)
|
||||||
|
### 🔵 Косметика (можно лучше)
|
||||||
|
### 📖 Best practices (как правильно)
|
||||||
|
|
||||||
|
Для каждой находки: **файл, проблема, решение, приоритет.**
|
||||||
|
|
||||||
|
В конце — 3-5 главных улучшений которые дадут максимальный эффект при минимуме правок.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# Результаты тестирования Polygon v0.4.3
|
||||||
|
|
||||||
|
Дата: 2026-08-01
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Python API-тесты: 35/35 PASS ✅
|
||||||
|
|
||||||
|
Файл: `polygon/tests/test_api.py`
|
||||||
|
Цель: `https://polygon.pythonk8s.dev.nubes.ru`
|
||||||
|
|
||||||
|
### 1. Health (1/1)
|
||||||
|
- ✅ `GET /health → 200 OK`
|
||||||
|
|
||||||
|
### 2. Services (4/4)
|
||||||
|
- ✅ `GET /services → 200, >=37 сервисов`
|
||||||
|
- ✅ `GET /services → каждый имеет svcId, svc, svcShort`
|
||||||
|
- ✅ `GET /services/1 → 200 + operations`
|
||||||
|
- ✅ `GET /services/99999 → 404`
|
||||||
|
|
||||||
|
### 3. Instances (10/10)
|
||||||
|
- ✅ `GET /instances (после reset) → total=0`
|
||||||
|
- ✅ `POST /instances (без serviceId) → 400`
|
||||||
|
- ✅ `POST /instances (serviceId=99999) → 404`
|
||||||
|
- ✅ `POST /instances → 201 + Location + instanceUid`
|
||||||
|
- ✅ `GET /instances → total=1`
|
||||||
|
- ✅ `GET /instances?pageSize=500 → pageSize=200 (clamped)`
|
||||||
|
- ✅ `GET /instances?page=-1 → не падает`
|
||||||
|
- ✅ `GET /instances/{uid} → status=creating, имя верное`
|
||||||
|
- ✅ `GET /instances/{uid}?fields=... → 200`
|
||||||
|
- ✅ `GET /instances/nonexistent → 404`
|
||||||
|
|
||||||
|
### 4. Operations (8/8)
|
||||||
|
- ✅ `GET /instanceOperations/default/18 → 200 + cfsParams`
|
||||||
|
- ✅ `POST /instanceOperations → 201 + Location`
|
||||||
|
- ✅ `GET /op/{uid} → dtStart=null, dtFinish=null, isSuccessful=null`
|
||||||
|
- ✅ `POST /instanceOperationCfsParams → 200`
|
||||||
|
- ✅ `GET /validate-cfs → 200`
|
||||||
|
- ✅ `POST /run → ok=true`
|
||||||
|
- ✅ `POST /run (повторно) → 409`
|
||||||
|
- ✅ `GET /op/{uid} (после run) → dtFinish!=null, isSuccessful=true`
|
||||||
|
|
||||||
|
### 5. Fail-next (4/4)
|
||||||
|
- ✅ `POST /_mock/fail-next → 200, fail_next=true`
|
||||||
|
- ✅ `POST /instances (fail-next) → 201`
|
||||||
|
- ✅ `POST /instanceOperations (fail-next) → 201`
|
||||||
|
- ✅ `POST /run (fail-next) → ok=false, error='mock failure'`
|
||||||
|
|
||||||
|
### 6. Auth (3/3 + 2 skipped)
|
||||||
|
- ⚠️ Auth отключена на проде (MOCK_AUTH_TOKEN не задан)
|
||||||
|
- ✅ `POST /_mock/reset (верный токен) → 200`
|
||||||
|
- ✅ `GET /services (без токена) → 200`
|
||||||
|
- ✅ `POST /instances (без токена) → 201`
|
||||||
|
|
||||||
|
### 7. Mock state (5/5)
|
||||||
|
- ✅ `GET /_mock/state → instances + operations`
|
||||||
|
- ✅ `GET /_mock/services → count >= 37`
|
||||||
|
- ✅ `POST /_mock/delay/0.1 → delay=0.1`
|
||||||
|
- ✅ `POST /_mock/delay/100 → 400`
|
||||||
|
- ✅ `POST /_mock/delay/-1 → 400`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Swagger UI (browser)
|
||||||
|
|
||||||
|
| Проверка | Результат |
|
||||||
|
|----------|-----------|
|
||||||
|
| Страница загружается | ✅ |
|
||||||
|
| Логотип Nubes + версия | ✅ |
|
||||||
|
| Ссылка «← На главную» | ✅ |
|
||||||
|
| Все 6 тегов | ✅ services, instances, operations, mock, health |
|
||||||
|
| Все 17 эндпоинтов | ✅ |
|
||||||
|
| Все 20 схем | ✅ |
|
||||||
|
| Спека — валидный JSON | ✅ |
|
||||||
|
| `security: []` (глобальный) | ✅ |
|
||||||
|
| `mockAuth` (apiKey, X-Mock-Auth) | ✅ |
|
||||||
|
| `_mock/*` имеют `security: [mockAuth]` | ✅ 5/5 |
|
||||||
|
| `dtStart/dtFinish` → `["string","null"]` | ✅ OAS 3.1.0 |
|
||||||
|
| `isSuccessful` → `["boolean","null"]` | ✅ |
|
||||||
|
| `RunResponse` → `ok + error` | ✅ |
|
||||||
|
| Кнопка Authorize | ⚠️ недоступна в browser-окружении |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Найденные расхождения (prod vs код)
|
||||||
|
|
||||||
|
| Проблема | Статус |
|
||||||
|
|----------|--------|
|
||||||
|
| Описание всё ещё говорит «Bearer-токен» | 🔧 Исправлено в коде, не задеплоено |
|
||||||
|
| Auth (MOCK_AUTH_TOKEN) не включена | ⚙️ Конфигурация деплоя |
|
||||||
|
| `pageSize` обрезается молча | ✅ Задокументировано в спеке |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Итого
|
||||||
|
|
||||||
|
- **API-тесты**: 35/35 PASS
|
||||||
|
- **Swagger UI**: страница работает, спека валидна, все эндпоинты и схемы на месте
|
||||||
|
- **Баги v0.4.2**: все 5 исправлены, проверены локально
|
||||||
|
- **К деплою**: закоммитить исправление описания auth, повысить версию, redeploy
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Ссылки: генератор YAML (tf_provider)
|
||||||
|
|
||||||
|
Репозиторий Terraform-провайдера Nubes: `~/tf_provider`
|
||||||
|
|
||||||
|
## Как генерируются STANDS YAML
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `~/tf_provider/README.md` | Общий пайплайн: 4 шага генерации провайдера |
|
||||||
|
| `~/tf_provider/docs/README.md` | **Главный индекс документации.** Формат токенов (не протухают), DDoS-Guard заголовки, Gateway URL |
|
||||||
|
| `~/tf_provider/docs/HOWTO_ADD_NEW_SERVICE.md` | Как добавить сервис: `services_list.txt` → `01_generate_yamls.sh` → API → YAML |
|
||||||
|
| `~/tf_provider/docs/ARCHITECTURE_NEW.md` | Архитектура: слои (core/provider/resources_gen/yaml), метод Виталия без instanceUid |
|
||||||
|
| `~/tf_provider/docs/70_api/api-discovery-algorithms.md` | **Ключевой документ.** Алгоритм получения параметров: `/instanceOperations/default/{id}` → `cfsParams` с `dataDescriptor`, `valueList`, `isModifiable` |
|
||||||
|
|
||||||
|
## Критические API-эндпоинты (метод Виталия)
|
||||||
|
|
||||||
|
Цепочка из 3 запросов — получение ВСЕХ параметров сервиса без создания инстанса:
|
||||||
|
|
||||||
|
1. `GET /api/v1/svc/services` → найти `svcId` по имени
|
||||||
|
2. `GET /api/v1/svc/services/{svcId}` → найти `svcOperationId` операции (create: `{svc.operations[?operation=="create"].svcOperationId}`)
|
||||||
|
3. `GET /api/v1/svc/instanceOperations/default/{svcOperationId}` → **все cfsParams**: `dataDescriptor` (подполя map-fixed), `valueList`, `isModifiable`, `isSensitive`, `regex`, ...
|
||||||
|
|
||||||
|
### Почему `/instanceOperations/default/{id}` а не `/serviceOperation/{id}`:
|
||||||
|
|
||||||
|
| Данные | `/serviceOperation/{id}` | `/instanceOperations/default/{id}` |
|
||||||
|
|--------|--------------------------|-------------------------------------|
|
||||||
|
| ID, code, тип, isRequired | ✅ | ✅ |
|
||||||
|
| `valueList` (допустимые значения) | ❌ | ✅ |
|
||||||
|
| `dataDescriptor` (подполя map-fixed) | ❌ | ✅ |
|
||||||
|
| `isModifiable` | ❌ | ✅ |
|
||||||
|
| `regex`, `maxLength`, `minValue`... | ❌ | ✅ |
|
||||||
|
| `isSensitive` | ❌ | ✅ |
|
||||||
|
|
||||||
|
### Формат valueList:
|
||||||
|
|
||||||
|
- Для **верхнеуровневых** параметров: JSON-массив `["false", "true"]`
|
||||||
|
- Для **подполей** dataDescriptor: comma-separated строка `"1,3,5,7"`
|
||||||
|
|
||||||
|
## Где взять реальные ответы API (для сверки мока)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Токены (НЕ протухают до декабря 2026):
|
||||||
|
~/tf_provider/secrets/test.token
|
||||||
|
~/tf_provider/secrets/dev.token
|
||||||
|
~/tf_provider/secrets/prod.token
|
||||||
|
|
||||||
|
# Проверить дату токена:
|
||||||
|
python3 -c "
|
||||||
|
import json,base64
|
||||||
|
t=open('$HOME/tf_provider/secrets/test.token').read().split('.')
|
||||||
|
d=json.loads(base64.urlsafe_b64decode(t[1]+'=='))
|
||||||
|
from datetime import datetime,timezone
|
||||||
|
print(datetime.fromtimestamp(d['exp'],tz=timezone.utc))
|
||||||
|
"
|
||||||
|
|
||||||
|
# Пример curl (test стенд):
|
||||||
|
curl -s --max-time 10 \
|
||||||
|
-H "Authorization: Bearer $(cat ~/tf_provider/secrets/test.token)" \
|
||||||
|
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \
|
||||||
|
-H "Referer: https://deck-test.ngcloud.ru/" \
|
||||||
|
"https://lk-api-gateway-test.ngcloud.ru/api/v1/svc/instanceOperations/default/<opId>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⛔ DDoS-Guard: ВСЕГДА нужны 3 заголовка
|
||||||
|
|
||||||
|
**Без них — 403 Forbidden даже с валидным токеном.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
|
||||||
|
-H "Referer: https://deck-{stand}.ngcloud.ru/"
|
||||||
|
```
|
||||||
|
|
||||||
|
### URL стендов (Gateway)
|
||||||
|
|
||||||
|
| Стенд | Gateway URL | Referer |
|
||||||
|
|-------|-------------|---------|
|
||||||
|
| test | `https://lk-api-gateway-test.ngcloud.ru/api/v1/svc` | `https://deck-test.ngcloud.ru/` |
|
||||||
|
| dev | `https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc` | `https://deck-dev.ngcloud.ru/` |
|
||||||
|
| prod | `https://lk-api-gateway.ngcloud.ru/api/v1/svc` | `https://deck.ngcloud.ru/` |
|
||||||
|
|
||||||
|
## Сгенерированные YAML (все стенды)
|
||||||
|
|
||||||
|
| Стенд | Сервисов | Путь |
|
||||||
|
|-------|----------|------|
|
||||||
|
| dev | 50 | `~/tf_provider/generated/dev/resources_yaml/` |
|
||||||
|
| test | 48 | `~/tf_provider/generated/test/resources_yaml/` |
|
||||||
|
| prod | 46 | `~/tf_provider/generated/prod/resources_yaml/` |
|
||||||
|
|
||||||
|
44 сервиса идентичны на всех трёх стендах.
|
||||||
|
|
||||||
|
## Поток данных: API → polygon
|
||||||
|
|
||||||
|
```
|
||||||
|
API Nubes (реальный)
|
||||||
|
→ 01_generate_yamls.sh
|
||||||
|
→ ~/tf_provider/generated/test/resources_yaml/*.yaml
|
||||||
|
→ копируются в STANDS/test/resources_yaml/ (autotest)
|
||||||
|
→ from_stands.py конвертирует
|
||||||
|
→ polygon/site/services/*.yaml
|
||||||
|
→ config_loader.py загружает при старте
|
||||||
|
→ polygon эмулирует /instanceOperations/default/{id}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user