Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba63d00308 |
@@ -1,25 +1,80 @@
|
|||||||
# Правила (читают ВСЕ агенты — Опус, Соннет, Fable, DeepSeek)
|
# ⛔ ПРАВИЛА ПРОЕКТА AUTOTEST
|
||||||
|
|
||||||
## ⛔ БЕЗ «ДЕЛАЙ» — НИЧЕГО НЕ ДЕЛАТЬ
|
## ⛔ ЗАПРЕТ НА sed .... ПОЛНЫЙ ЗАПРЕТ НА SED
|
||||||
Ни кода, ни git, ни curl, ни терминала, ни kubectl. Только смотреть и отвечать словами.
|
Только `replace_string_in_file` или `create_file`. sed не использовать НИКОГДА.
|
||||||
Ждать явного «делай», «да», «пушь», «коммитить». НЕТ «делай» — НЕТ действий.
|
НИКОГДА НЕ ИСПОЛЬЗОВАТЬ 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/YYYY-MM-DD-session.md`. Commit + push после записи.
|
Всё что обсуждаем и делаем — записывать в `/home/naeel/nubes/autotest/HISTORY/`.
|
||||||
|
Формат: `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,7 +8,6 @@ secrets/*
|
|||||||
|
|
||||||
# Submodules / nested repos
|
# Submodules / nested repos
|
||||||
app-autotest/
|
app-autotest/
|
||||||
polygon/
|
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -20,7 +19,6 @@ Thumbs.db
|
|||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
.venv/
|
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
# Go (embed.go generates from yaml)
|
# Go (embed.go generates from yaml)
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# Архитектура Autotest — полный обзор
|
|
||||||
|
|
||||||
v1.1.44, 2026-07-30
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Платформа для тестирования Nubes Cloud API — ручное и автоматизированное создание/изменение/удаление инстансов любых сервисов (PostgreSQL, Redis, S3, Kafka, ~35 сервисов).
|
|
||||||
|
|
||||||
## Развёртывание
|
|
||||||
|
|
||||||
- **Платформа:** Nubes pythonk8s (managed Flask + gunicorn)
|
|
||||||
- **URL:** `https://atest.pythonk8s.dev.nubes.ru/`
|
|
||||||
- **БД:** PostgreSQL 17 (Zalando Operator), autotest
|
|
||||||
- **Репозиторий:** `/home/naeel/nubes/autotest/app-autotest`
|
|
||||||
- **Родительский репо:** `/home/naeel/nubes/autotest`
|
|
||||||
|
|
||||||
## Ключевые файлы
|
|
||||||
|
|
||||||
### Бэкенд (Flask)
|
|
||||||
| Файл | Назначение |
|
|
||||||
|------|-----------|
|
|
||||||
| `site/app.py` | Точка входа, регистрация blueprint'ов, VERSION |
|
|
||||||
| `site/routes/api_test.py` | Основной: POST /api/test, GET /api/params, поллинг, _finish_op |
|
|
||||||
| `site/routes/main.py` | GET /, /api/operations/{svc_id} |
|
|
||||||
| `site/routes/api.py` | LEGACY эндпоинты (будет удалён) |
|
|
||||||
| `site/api/http_client.py` | HTTP-клиент Nubes API (GET/POST/raw_delete) |
|
|
||||||
| `site/api/auth.py` | Токены, get_client, get_stand |
|
|
||||||
| `site/operations/get_params.py` | get_params_with_current_values, _normalize_value_list, state.out |
|
|
||||||
| `site/operations/get_instances.py` | GET /instances с пагинацией |
|
|
||||||
| `site/operations/get_services.py` | GET /services, /services/{id} |
|
|
||||||
| `site/operations/service_list.py` | Загрузка services_{stand}.txt |
|
|
||||||
| `site/operations/tracker.py` | fcntl.flock файловый трекер инстансов |
|
|
||||||
| `site/db/pool.py` | psycopg2 ThreadedConnectionPool (lazy-init) |
|
|
||||||
| `site/db/init_db.py` | CREATE TABLE runs + миграции |
|
|
||||||
| `site/db/save_run.py` | INSERT в runs (16 колонок) |
|
|
||||||
| `site/runner.py` | LEGACY runner (будет удалён) |
|
|
||||||
| `site/config.yaml` | LEGACY конфиг (будет удалён) |
|
|
||||||
|
|
||||||
### Фронтенд (vanilla JS)
|
|
||||||
| Файл | Назначение |
|
|
||||||
|------|-----------|
|
|
||||||
| `site/templates/index.html` | Jinja2 шаблон, CSS, window.APP |
|
|
||||||
| `site/static/app.js` | Весь фронтенд (460 строк) |
|
|
||||||
| `site/static/style.css` | Доп. стили |
|
|
||||||
|
|
||||||
### Конфиги
|
|
||||||
| Файл | Назначение |
|
|
||||||
|------|-----------|
|
|
||||||
| `site/config/services_test.txt` | Список сервисов TEST (комментировать # для исключения) |
|
|
||||||
| `site/config/services_dev.txt` | Список сервисов DEV |
|
|
||||||
| `requirements.txt` | Python-зависимости |
|
|
||||||
|
|
||||||
## CREATE flow (9 шагов Nubes API)
|
|
||||||
|
|
||||||
1. POST /instances
|
|
||||||
2. POST /instanceOperations {operation:"create"}
|
|
||||||
3. GET /instanceOperations/{opUid}?fields=cfsParams
|
|
||||||
4. resolveRefSvcParamValues
|
|
||||||
5. POST /instanceOperationCfsParams (пользовательские)
|
|
||||||
6. POST /instanceOperationCfsParams (required+default)
|
|
||||||
7. GET /validate-cfs
|
|
||||||
8. POST /run
|
|
||||||
9. Поллинг dtFinish
|
|
||||||
|
|
||||||
## Фронтенд — глобальное состояние
|
|
||||||
|
|
||||||
```
|
|
||||||
svcInstances — кеш инстансов
|
|
||||||
selectedInst — UID выбранного
|
|
||||||
selectedOp — {opId, opName, svcId}
|
|
||||||
pollTimer — setInterval поллинга
|
|
||||||
currentSvcId — ID сервиса
|
|
||||||
currentSvcName — имя сервиса
|
|
||||||
currentSvcShort— краткое имя (redis, postgres)
|
|
||||||
busy — блокировка параллельных операций
|
|
||||||
```
|
|
||||||
|
|
||||||
## План развития (MVP сценариев)
|
|
||||||
|
|
||||||
Файлы: `/home/naeel/nubes/autotest/TASKS/3007.md` (требования), `/home/naeel/nubes/autotest/DOCS/sol-answers.md` (решения)
|
|
||||||
|
|
||||||
1. Багфиксы аудита
|
|
||||||
2. Удалить legacy (api.py, runner.py, config.yaml)
|
|
||||||
3. Сервисные токены для сценариев
|
|
||||||
4. YAML-сценарии + валидатор
|
|
||||||
5. scenario_runs таблица + runner
|
|
||||||
6. UI: сворачиваемая секция «Сценарии»
|
|
||||||
|
|
||||||
## Документация
|
|
||||||
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md` — эталонный Nubes API flow
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/sol-answers.md` — ответы на 14 вопросов по архитектуре
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/questions-to-sol.md` — 14 вопросов к Sol
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/DOCS/sonnet-final-audit.md` — финальный аудит Соннета
|
|
||||||
- `/home/naeel/nubes/autotest/HISTORY/2026-07-29-session.md` — хронология правок
|
|
||||||
@@ -1,417 +0,0 @@
|
|||||||
# ⚠️ LEGACY — НЕАКТУАЛЬНО. См. ARCHITECTURE.md для текущего состояния.
|
|
||||||
|
|
||||||
# Архитектура app-autotest — полный документ (v1.0.76)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Обзор
|
|
||||||
|
|
||||||
Flask-приложение для автотестов операций сервисов облачной платформы Nubes.
|
|
||||||
Деплой: Nubes pythonk8s (gunicorn), тестовый стенд `atest.pythonk8s.dev.nubes.ru`.
|
|
||||||
Репозиторий: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`.
|
|
||||||
|
|
||||||
Текущий срез потока операций:
|
|
||||||
- CREATE и обычные операции разделены в UI и backend.
|
|
||||||
- CREATE использует фиксированный префикс `autotest-`, non-create работает по выбранному инстансу.
|
|
||||||
- Финальный статус показывает статус, операцию, длительность и имя инстанса.
|
|
||||||
- Список autotest-инстансов строится cloud-first; tracker нужен только как короткий fallback после CREATE.
|
|
||||||
- **Modify** (и другие операции с параметрами) показывает **текущие** значения параметров инстанса: создаётся pending-операция, читаются `cfsParams.paramValue`, форма заполняется реальными данными. При submit операция переиспользуется через `previewOpUid`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Структура файлов
|
|
||||||
|
|
||||||
```
|
|
||||||
app-autotest/
|
|
||||||
├── site/ ← НЕ пакет (без __init__.py, конфликт с stdlib)
|
|
||||||
│ ├── app.py ← Flask(__name__), VERSION, blueprints, /health
|
|
||||||
│ ├── api/
|
|
||||||
│ │ └── http_client.py ← HttpClient + STANDS + detect_endpoint()
|
|
||||||
│ ├── operations/
|
|
||||||
│ │ ├── tracker.py ← Трекер: /tmp/instances.json + fcntl.flock
|
|
||||||
│ │ ├── get_services.py ← get_services(), get_service_detail()
|
|
||||||
│ │ └── get_instances.py ← get_instances(), get_organization()
|
|
||||||
│ ├── routes/
|
|
||||||
│ │ ├── main.py ← GET/POST / (главная), _tmpl()
|
|
||||||
│ │ ├── api.py ← /api/run, /api/status, /api/config
|
|
||||||
│ │ └── api_test.py ← /api/test, /api/test/status, _finish_op
|
|
||||||
│ ├── templates/
|
|
||||||
│ │ └── index.html ← UI: Jinja2 + JS (поллинг)
|
|
||||||
│ └── static/
|
|
||||||
│ └── style.css
|
|
||||||
└── secrets/
|
|
||||||
├── dev.token ← Токен для DEV стенда
|
|
||||||
├── test.token ← Токен для TEST стенда
|
|
||||||
└── test.token.new
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Поток CREATE — полная трассировка (14 шагов)
|
|
||||||
|
|
||||||
### Шаг 1: UI — пользователь начинает CREATE
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// index.html: startCreate()
|
|
||||||
function startCreate(){
|
|
||||||
selectedInst=null; // нет выбранного инстанса
|
|
||||||
stopPoll(); // остановить предыдущий поллинг
|
|
||||||
document.querySelectorAll('[data-iuid]').forEach(e=>e.classList.remove('active'));
|
|
||||||
document.querySelectorAll('.inst-ops').forEach(e=>e.classList.remove('open'));
|
|
||||||
showParams(18,'create'); // 18 = svcOperationId для create у сервиса "Болванка"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`selectedOp = {opId: 18, opName: "create", svcId: 1}`
|
|
||||||
|
|
||||||
### Шаг 2: UI — загрузка параметров и показ формы
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// index.html: showParams(18, 'create')
|
|
||||||
fetch('/api/params/18') // GET /api/params/18
|
|
||||||
.then(r=>r.json())
|
|
||||||
.then(params=>{
|
|
||||||
// Генерация формы: displayName + поля из params
|
|
||||||
const displayName = 'autotest-1-' + Date.now().toString(36);
|
|
||||||
form.innerHTML = '<input id="param-displayname" value="' + displayName + '">'
|
|
||||||
+ params.map(p => '<input name="p_' + p.svcOperationCfsParamId + '">').join('');
|
|
||||||
|
|
||||||
btn.onclick = () => {
|
|
||||||
// Сбор params из формы
|
|
||||||
const pp = {};
|
|
||||||
document.querySelectorAll('#params-form [name^="p_"]').forEach(el => {
|
|
||||||
pp[el.name.replace('p_','')] = el.value;
|
|
||||||
});
|
|
||||||
executeOp(pp); // ← запуск
|
|
||||||
};
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Бэкенд: `GET /instanceOperations/default/18` → возвращает список `cfsParams`.
|
|
||||||
|
|
||||||
### Шаг 3: UI — executeOp (отправка)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// index.html: executeOp(params)
|
|
||||||
const displayName = document.getElementById('param-displayname')?.value || 'autotest-1';
|
|
||||||
// Захват ДО очистки формы! (v1.0.48)
|
|
||||||
|
|
||||||
document.getElementById('params-form').innerHTML = ''; // очистка
|
|
||||||
|
|
||||||
fetch('/api/test', {
|
|
||||||
method:'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
serviceId: 1, // SVC_ID
|
|
||||||
operation: "create",
|
|
||||||
svcOperationId: 18,
|
|
||||||
params: {...}, // собранные параметры
|
|
||||||
instanceUid: "", // пусто для create
|
|
||||||
displayName: displayName
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
JSON: `{"serviceId":1, "operation":"create", "svcOperationId":18, "params":{...}, "instanceUid":"", "displayName":"autotest-1-lq5x3a"}`
|
|
||||||
|
|
||||||
### Шаг 4: Бэкенд — api_test() разбор запроса
|
|
||||||
|
|
||||||
```python
|
|
||||||
# api_test.py: POST /api/test
|
|
||||||
data = request.get_json()
|
|
||||||
svc_id = data["serviceId"] # 1 (int)
|
|
||||||
op_name = data["operation"] # "create" (str)
|
|
||||||
svc_op_id = data["svcOperationId"] # 18 (int)
|
|
||||||
params = data.get("params", {}) # {...} (dict)
|
|
||||||
instance_uid = data.get("instanceUid") # "" (falsy)
|
|
||||||
display_name = data.get("displayName", f"autotest-{svc_id}") # "autotest-1-lq5x3a"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 5: Бэкенд — _client() с автоопределением стенда
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _client():
|
|
||||||
token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
|
|
||||||
endpoint = detect_endpoint(token) or current_app.config["NUBES_API_ENDPOINT"]
|
|
||||||
return HttpClient(endpoint, token)
|
|
||||||
```
|
|
||||||
|
|
||||||
`detect_endpoint(token)` пробует dev→test стенды, возвращает рабочий URL или None.
|
|
||||||
|
|
||||||
### Шаг 6: Бэкенд — создание инстанса в Nubes
|
|
||||||
|
|
||||||
```python
|
|
||||||
payload = {"serviceId": 1, "displayName": "autotest-1-lq5x3a", "descr": ""}
|
|
||||||
resp = client.post("/instances", payload)
|
|
||||||
# Ответ: {"instanceUid": "93b0b65d-..."}
|
|
||||||
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(...)
|
|
||||||
# → "93b0b65d-e080-416d-bbb8-463f7adbda80"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 7: Бэкенд — создание операции
|
|
||||||
|
|
||||||
```python
|
|
||||||
op_payload = {"instanceUid": "93b0b65d-...", "operation": "create"}
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
|
||||||
# Ответ: {"instanceOperationUid": "0b0596d8-..."}
|
|
||||||
op_uid = _find_uid(op_resp) or _uid_from_location(...)
|
|
||||||
# _find_uid ищет: instanceOperationUid → instanceUid → uid → Uid
|
|
||||||
# → "0b0596d8-b48d-4694-98c3-65f2f8eeb27d"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 8: Бэкенд — tracker_add (v1.0.54: ДО params!)
|
|
||||||
|
|
||||||
```python
|
|
||||||
try:
|
|
||||||
tracker_add("93b0b65d-...", 1, "autotest-1-lq5x3a")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[TRACKER ERROR] ...")
|
|
||||||
```
|
|
||||||
|
|
||||||
`tracker.py`: `_locked_read()` → `data["93b0b65d-..."] = {...}` → `_locked_write(data)`.
|
|
||||||
Файл `/tmp/instances.json` обновлён под `fcntl.LOCK_EX | LOCK_NB`.
|
|
||||||
|
|
||||||
### Шаг 9: Бэкенд — установка параметров и запуск
|
|
||||||
|
|
||||||
```python
|
|
||||||
for pid, pval in params.items():
|
|
||||||
client.post("/instanceOperationCfsParams", {
|
|
||||||
"instanceOperationUid": op_uid,
|
|
||||||
"svcOperationCfsParamId": int(pid),
|
|
||||||
"paramValue": str(pval)
|
|
||||||
})
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
|
||||||
```
|
|
||||||
|
|
||||||
Если params пустые (например, повторный клик на «Готово») → `/run` → 422 "required CFS parameter ... is missing".
|
|
||||||
|
|
||||||
### Шаг 10: Бэкенд — фоновый поток
|
|
||||||
|
|
||||||
```python
|
|
||||||
threading.Thread(
|
|
||||||
target=_finish_op,
|
|
||||||
args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True),
|
|
||||||
daemon=True
|
|
||||||
).start()
|
|
||||||
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 11: UI — поллинг статуса
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// index.html: executeOp() — после получения RUNNING
|
|
||||||
pollTimer = setInterval(async () => {
|
|
||||||
const sr = await fetch('/api/test/status/' + opUid);
|
|
||||||
const sd = await sr.json();
|
|
||||||
showStages(sd.stages || []);
|
|
||||||
if (sd.status !== 'RUNNING') {
|
|
||||||
stopPoll();
|
|
||||||
// показать OK/FAIL
|
|
||||||
btn.textContent = 'Готово';
|
|
||||||
btn.disabled = false;
|
|
||||||
// ⚠️ БАГ: btn.onclick всё ещё активен!
|
|
||||||
if (sd.status === 'OK') {
|
|
||||||
await refreshInstances();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 12: Бэкенд — _finish_op (фоновый)
|
|
||||||
|
|
||||||
```python
|
|
||||||
while time.time() < deadline: # 300 секунд
|
|
||||||
data = client.get(f"/instanceOperations/{op_uid}?fields=...")
|
|
||||||
op = data.get("instanceOperation", {})
|
|
||||||
dt_finish = op.get("dtFinish")
|
|
||||||
|
|
||||||
_op_results[op_uid] = {"status": "RUNNING", "stages": [...], "duration": ...}
|
|
||||||
|
|
||||||
if dt_finish and str(dt_finish).strip():
|
|
||||||
is_ok = op.get("isSuccessful")
|
|
||||||
# tracker_add уже вызван синхронно!
|
|
||||||
if is_ok and is_delete:
|
|
||||||
tracker_remove(instance_uid)
|
|
||||||
_op_results[op_uid] = {"status": "OK" if is_ok else "FAIL", ...}
|
|
||||||
return
|
|
||||||
time.sleep(5)
|
|
||||||
```
|
|
||||||
|
|
||||||
Весь цикл обёрнут в `try/except: print(traceback)` (v1.0.51).
|
|
||||||
|
|
||||||
### Шаг 13: UI — refreshInstances
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
async function refreshInstances(){
|
|
||||||
const r = await fetch('/api/operations/1');
|
|
||||||
const d = await r.json();
|
|
||||||
const newInsts = d.instances || [];
|
|
||||||
// Найти новые (отсутствуют в svcInstances)
|
|
||||||
const added = newInsts.filter(i => !oldUids.has(i.instanceUid));
|
|
||||||
// Обновить бейджи существующих
|
|
||||||
// Добавить новые в DOM перед кнопкой "+ Создать"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 14: Бэкенд — api_operations (GET /api/operations/1)
|
|
||||||
|
|
||||||
```python
|
|
||||||
tracked = tracker_list() # читает /tmp/instances.json
|
|
||||||
tracked_by_uid = {uid: item for uid in tracked if svcId == 1}
|
|
||||||
instances = get_instances(_client()) # запрос в Nubes API
|
|
||||||
nubes_uids = {i["instanceUid"] for i in instances}
|
|
||||||
|
|
||||||
# Инстансы из Nubes которые есть в трекере (кроме deleted)
|
|
||||||
svc_instances = [i for i in instances
|
|
||||||
if i.get("instanceUid") in tracked_uids
|
|
||||||
and i.get("explainedStatus") not in ("deleted",)]
|
|
||||||
|
|
||||||
# Инстансы из трекера которых Nubes ещё не отдаёт → status "creating"
|
|
||||||
for uid, t in tracked_by_uid.items():
|
|
||||||
if uid not in nubes_uids:
|
|
||||||
svc_instances.append({
|
|
||||||
"instanceUid": uid,
|
|
||||||
"displayName": t["displayName"],
|
|
||||||
"explainedStatus": "creating",
|
|
||||||
...
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Трекер инстансов
|
|
||||||
|
|
||||||
### 4.1 Назначение
|
|
||||||
|
|
||||||
Хранит UID инстансов, созданных приложением. Нужен чтобы показывать в UI только «наши» инстансы, а не все в организации.
|
|
||||||
|
|
||||||
### 4.2 Реализация (v1.0.53+)
|
|
||||||
|
|
||||||
Файл: `site/operations/tracker.py`
|
|
||||||
|
|
||||||
- Хранилище: `/tmp/instances.json` (JSON-файл)
|
|
||||||
- Блокировка: `fcntl.flock(fd, LOCK_EX | LOCK_NB)` с retry до 2 сек
|
|
||||||
- Seed: `_INITIAL` (4 инстанса) при пустом/битом файле
|
|
||||||
- Функции: `add(uid, svc_id, name)`, `remove(uid)`, `list_all()`
|
|
||||||
|
|
||||||
### 4.3 Эволюция
|
|
||||||
|
|
||||||
| Версия | Реализация | Проблема |
|
|
||||||
|--------|-----------|----------|
|
|
||||||
| v1.0.45 | `/tmp/instances.json` + `threading.Lock` | `except: pass` скрывал ошибки |
|
|
||||||
| v1.0.49 | In-memory dict | Multi-worker gunicorn: у каждого свой dict |
|
|
||||||
| v1.0.53 | `/tmp/instances.json` + `fcntl.flock(LOCK_EX)` | Без таймаута: потенциальный зависон |
|
|
||||||
| v1.0.54 | `/tmp/instances.json` + `fcntl.flock(LOCK_EX\|LOCK_NB)` + retry 2s | ✅ Текущий |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Автоопределение стенда
|
|
||||||
|
|
||||||
Файл: `site/api/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",
|
|
||||||
]
|
|
||||||
|
|
||||||
def detect_endpoint(token):
|
|
||||||
for ep in STANDS:
|
|
||||||
try:
|
|
||||||
c = HttpClient(ep, token)
|
|
||||||
data = c.get("/instances", params={"pageSize": 1, "page": 1})
|
|
||||||
if data.get("results") is not None:
|
|
||||||
return ep
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
return None
|
|
||||||
```
|
|
||||||
|
|
||||||
Используется в:
|
|
||||||
- `main.py`: при загрузке главной страницы
|
|
||||||
- `api_test.py: _client()`: при каждом API-запросе
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Все известные баги (найдено/исправлено)
|
|
||||||
|
|
||||||
### Исправленные
|
|
||||||
|
|
||||||
| # | Версия | Баг | Причина | Исправление |
|
|
||||||
|---|--------|-----|---------|-------------|
|
|
||||||
| 1 | v1.0.46 | `_finish_op()` недостающие параметры | Не передавались `op_name`, `svc_op_id` | Добавлены в сигнатуру |
|
|
||||||
| 2 | v1.0.48 | displayName терялся | `params-form.innerHTML=''` до чтения `param-displayname` | Захват до очистки |
|
|
||||||
| 3 | v1.0.48 | tracker_add в daemon-потоке | Поток умирает под gunicorn | Перенос в синхронный код |
|
|
||||||
| 4 | v1.0.50 | ❌ для этапов в процессе | `isSuccessful===false` для in-progress | Проверка `dtFinish` |
|
|
||||||
| 5 | v1.0.50 | Инстанс не в списке | `explainedStatus:"not created"` фильтровался | Убран фильтр + tracked-сироты |
|
|
||||||
| 6 | v1.0.50 | `selectService()` скрывал stages | Полный перерендер после OK | `refreshInstances()` с диффом |
|
|
||||||
| 7 | v1.0.51 | `instance_groups` UnboundLocalError | Не иниц. до `if active_token:` | `instance_groups = {}` |
|
|
||||||
| 8 | v1.0.51 | `_finish_op` молча умирал | `data.get()` вне try/except | Весь цикл в try/except |
|
|
||||||
| 9 | v1.0.51 | Автостенд только в main.py | `_client()` в api_test.py не использовал | `detect_endpoint()` в http_client.py |
|
|
||||||
| 10 | v1.0.52 | `token_info` UndefinedError | Не передан в шаблон при action=clear | `_tmpl()` helper со всеми переменными |
|
|
||||||
| 11 | v1.0.53 | In-memory dict + multi-worker | У каждого воркера свой `_data` | `/tmp/instances.json` + `fcntl.flock` |
|
|
||||||
| 12 | v1.0.54 | `_find_uid()` возвращал не тот UUID | Итерация по всем значениям dict | Поиск по ключам: instanceOperationUid→instanceUid |
|
|
||||||
| 13 | v1.0.54 | Сироты при ошибке params | `tracker_add` ПОСЛЕ params loop | `tracker_add` ДО params loop |
|
|
||||||
| 14 | v1.0.54 | `flock` без таймаута | `LOCK_EX` блокируется навсегда | `LOCK_EX \| LOCK_NB` + retry 2s |
|
|
||||||
| 15 | v1.0.55 | F5 = предупреждение браузера | clear/stray POST → HTML без редиректа | `redirect("/")` для всех POST |
|
|
||||||
| 16 | v1.0.56 | Кнопка «Готово» запускала повторный CREATE | `btn.onclick` оставался привязанным после завершения операции | `btn.onclick = null` после финального статуса |
|
|
||||||
| 17 | v1.0.57 | Хрупкость финального состояния кнопки | Повторяемый код финализации статуса | `setFinishedState()` централизует финальный UI-состояние |
|
|
||||||
| 18 | v1.0.59 | После F5 список терял tracked-инстанс | `api_operations()` не добирал tracked-сирот обратно из трекера | `tracked_by_uid` + добавление отсутствующих tracked-инстансов |
|
|
||||||
| 19 | v1.0.60 | CREATE не появлялся сразу в списке после OK | UI делал diff и мог не перерисовать список целиком | Полная перерисовка `inst-list` в `refreshInstances()` |
|
|
||||||
| 20 | v1.0.63 | Дубли и чужие инстансы | Принадлежность определялась не по namespace | `autotest-` prefix + уникализация `displayName` при CREATE |
|
|
||||||
| 21 | v1.0.64 | Версия не показывалась в шапке | `load_config()` затирал VERSION | `config["VERSION"] = current_app.config["VERSION"]` |
|
|
||||||
| 22 | v1.0.64 | displayName был editable value, а не placeholder | CREATE подставлял имя в value | Placeholder `autotest-...`, значение вводит пользователь |
|
|
||||||
| 23 | v1.0.65 | `creating` из-за недочитанной страницы | `get_instances()` доверял `total` и мог не перейти на следующую страницу | Остановка по `len(batch) < pageSize` |
|
|
||||||
|
|
||||||
### ✅ Закрыто в v1.0.56
|
|
||||||
|
|
||||||
| Баг | Статус | Что поменяли |
|
|
||||||
|-----|--------|--------------|
|
|
||||||
| Кнопка «Готово» запускала повторный CREATE | закрыто | После финального статуса обработчик снимается через `btn.onclick = null` |
|
|
||||||
| displayName "autotest-1" при повторном клике | закрыто | Повторный клик больше не вызывает `executeOp()` на очищенной форме |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. HTTP-клиент
|
|
||||||
|
|
||||||
Файл: `site/api/http_client.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
class HttpClient:
|
|
||||||
def __init__(self, endpoint, token):
|
|
||||||
# Bearer auth + User-Agent: Mozilla/5.0 (DDoS-Guard)
|
|
||||||
|
|
||||||
def get(self, path, timeout=10):
|
|
||||||
# GET → r.raise_for_status() → r.json()
|
|
||||||
|
|
||||||
def post(self, path, data, timeout=30):
|
|
||||||
# POST → проверка r.ok → парсинг JSON → Location header
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Шаблон Jinja2 — используемые переменные
|
|
||||||
|
|
||||||
| Переменная | Где | Назначение |
|
|
||||||
|-----------|-----|-----------|
|
|
||||||
| `config.VERSION` | Строка 32 | Версия в топбаре |
|
|
||||||
| `token_info.email` | Строка 33 | Email пользователя |
|
|
||||||
| `token_info.company` | Строка 33 | Компания |
|
|
||||||
| `env_token_masked` | Строка 36 | Маскированный токен в placeholder |
|
|
||||||
| `has_user_token` | Строка 36 | Показать токен или маску |
|
|
||||||
| `error` | Строка 40 | Ошибка |
|
|
||||||
| `organization.displayName` | Строка 50 | Название организации |
|
|
||||||
| `organization.explainedStatus` | Строка 51 | Статус организации |
|
|
||||||
| `client_id` | Строка 50 | ID клиента |
|
|
||||||
| `instance_groups` | Строки 55-62 | Группы инфраструктурных инстансов |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Ограничения платформы Nubes pythonk8s
|
|
||||||
|
|
||||||
- `site/` — НЕ пакет (без `__init__.py`, конфликт с stdlib `site.py`)
|
|
||||||
- Импорты: `from api.http_client import ...` (без префикса `site.`)
|
|
||||||
- `app.run(host="0.0.0.0", port=5000)` — обязательно
|
|
||||||
- Gunicorn: запускается платформой, количество воркеров неизвестно
|
|
||||||
- Нет persistent volume — `/tmp/` теряется при редеплое
|
|
||||||
- User-Agent: `Mozilla/5.0` обязателен (DDoS-Guard)
|
|
||||||
- Деплой: `git push` → managed service редеплоит
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
# Архитектура app-autotest — текущее состояние
|
|
||||||
|
|
||||||
v1.0.93, 28.07.2026
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Обзор
|
|
||||||
|
|
||||||
Flask-приложение (один HTML-файл, без SPA-фреймворка) для ручного тестирования операций
|
|
||||||
сервисов облачной платформы Nubes. Работает через REST API Nubes, автоопределяет стенд
|
|
||||||
(dev/test) по JWT-токену.
|
|
||||||
|
|
||||||
**Деплой**: Nubes pythonk8s (gunicorn, несколько воркеров).
|
|
||||||
**Кластер**: `iot-naeel` (K8s resourceRealm, DEV-стенд)
|
|
||||||
**URL**: `https://atest.pythonk8s.dev.nubes.ru`
|
|
||||||
**Репозиторий**: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`
|
|
||||||
|
|
||||||
### Ключевые принципы
|
|
||||||
|
|
||||||
1. **Источник правды — Nubes API**. Все данные (инстансы, параметры, статусы) получаются
|
|
||||||
только из API. Никаких локальных копий или кеша (кроме краткосрочного трекера).
|
|
||||||
2. **Разделение CREATE и non-CREATE**. Это два принципиально разных потока.
|
|
||||||
3. **Multi-worker safety**. Все общие ресурсы (/tmp файлы) защищены `fcntl.flock`.
|
|
||||||
4. **Мелкие функции**. Каждое действие — отдельная функция.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Структура файлов
|
|
||||||
|
|
||||||
```
|
|
||||||
app-autotest/
|
|
||||||
├── site/ ← НЕ пакет (без __init__.py)
|
|
||||||
│ ├── app.py ← Flask(__name__), VERSION, blueprints, /health
|
|
||||||
│ ├── runner.py ← [LEGACY] Раннер тестов
|
|
||||||
│ ├── config.yaml ← [LEGACY] Конфиг раннера
|
|
||||||
│ ├── api/
|
|
||||||
│ │ └── http_client.py ← HttpClient, автостенд
|
|
||||||
│ ├── operations/
|
|
||||||
│ │ ├── get_services.py ← get_services(), get_service_detail()
|
|
||||||
│ │ ├── get_instances.py ← get_instances() (пагинация), get_organization()
|
|
||||||
│ │ ├── get_params.py ← get_params_with_current_values() — слияние state.params + шаблон
|
|
||||||
│ │ └── tracker.py ← /tmp/instances-{clientId}-{stand}.json + flock
|
|
||||||
│ ├── routes/
|
|
||||||
│ │ ├── main.py ← GET/POST /, /api/operations/<svc_id>
|
|
||||||
│ │ ├── api.py ← [LEGACY] /api/run, /api/status, /api/config
|
|
||||||
│ │ └── api_test.py ← /api/test, /api/params, /api/log
|
|
||||||
│ ├── templates/
|
|
||||||
│ │ └── index.html ← Весь UI (Jinja2 + ванильный JS)
|
|
||||||
│ └── static/
|
|
||||||
│ ├── style.css
|
|
||||||
│ ├── logo.svg
|
|
||||||
│ └── favicon.svg
|
|
||||||
└── secrets/
|
|
||||||
├── dev.token
|
|
||||||
└── test.token
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Бэкенд: HTTP-клиент и автостенд
|
|
||||||
|
|
||||||
### `http_client.py`
|
|
||||||
|
|
||||||
- `HttpClient(endpoint, token)` — обёртка над `requests.Session`:
|
|
||||||
- Заголовки: `Authorization: Bearer`, `User-Agent: Mozilla/5.0` (DDoS-Guard)
|
|
||||||
- `get(path)` → `raise_for_status()` → `.json()`
|
|
||||||
- `post(path, data)` → проверка `r.ok`, парсинг JSON, извлечение `Location`
|
|
||||||
- `detect_endpoint(token)` — пробует dev→test стенды, возвращает рабочий URL
|
|
||||||
- `stand_name(endpoint)` — "dev"/"test" по URL
|
|
||||||
- `create_client(token, fallback)` — HttpClient + endpoint
|
|
||||||
|
|
||||||
### `get_instances.py`
|
|
||||||
|
|
||||||
- `get_instances(client)` — `GET /instances` с пагинацией (pageSize=200). Остановка по `len(batch) < pageSize`.
|
|
||||||
- `get_organization(client)` — первый инстанс с `serviceId == 19`
|
|
||||||
|
|
||||||
### `get_services.py`
|
|
||||||
|
|
||||||
- `get_services(client)` → `GET /services` → список
|
|
||||||
- `get_service_detail(client, svc_id)` → `GET /services/{id}` → детали + операции
|
|
||||||
|
|
||||||
### `get_params.py` — КЛЮЧЕВОЙ модуль
|
|
||||||
|
|
||||||
`get_params_with_current_values(client, op_id, instance_uid)`:
|
|
||||||
1. `GET /instances/{uid}` → `state.params` (текущие значения: `{"whereFail":"1",...}`)
|
|
||||||
2. `GET /instanceOperations/default/{opId}` → шаблон (коды, типы, valueList, dataDescriptor)
|
|
||||||
3. Слияние: `defaultValue = state.params["код"] ?? template.defaultValue`
|
|
||||||
4. Приведение типов: `bool` → `"true"/"false"`, `dict` → `json.dumps()`
|
|
||||||
|
|
||||||
### `tracker.py`
|
|
||||||
|
|
||||||
- Файл: `/tmp/instances-{clientId}-{stand}.json` (изолирован по пользователю и стенду)
|
|
||||||
- Блокировка: `fcntl.flock(LOCK_EX | LOCK_NB)` + retry 2s
|
|
||||||
- `add(client_id, stand, uid, svc_id, name)`, `remove(...)`, `list_all(...)`
|
|
||||||
- Нужен ТОЛЬКО как fallback — когда инстанс только создан и ещё не в `/instances`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Бэкенд: Роуты
|
|
||||||
|
|
||||||
### `main.py`
|
|
||||||
|
|
||||||
- **`GET/POST /`** — главная: организация, инфраструктура (serviceId 2,12,19,21,22,25,26,29,110,150), сервисы
|
|
||||||
- `action=save` → сохранить токен в cookie
|
|
||||||
- `action=clear` → удалить cookie
|
|
||||||
- **`GET /api/operations/<svc_id>`** — операции + autotest-инстансы:
|
|
||||||
- Cloud-first: фильтр по префиксу `autotest-`
|
|
||||||
- Tracker-fallback: статус "creating" для ещё невидимых
|
|
||||||
|
|
||||||
### `api_test.py` — основной модуль операций
|
|
||||||
|
|
||||||
- **`GET /api/params/<op_id>[?instanceUid=xxx]`**:
|
|
||||||
- Без `instanceUid` → шаблонные `defaultValue`
|
|
||||||
- С `instanceUid` → `get_params_with_current_values()`
|
|
||||||
- **`POST /api/test`** — запуск операции:
|
|
||||||
- **CREATE**: `POST /instances` → `instanceUid` → `POST /instanceOperations` → `opUid` → `tracker_add` → params → `/run`
|
|
||||||
- **non-CREATE**: `POST /instanceOperations` → params → `/run` (redeploy: сразу `/run`)
|
|
||||||
- `displayName` = `_get_instance_display_name()` (из API, не UUID!)
|
|
||||||
- Фоновый поток: `_finish_op()` — поллинг до `dtFinish` (300s таймаут)
|
|
||||||
- **`GET /api/test/status/<op_uid>`** — поллинг: in-memory `_op_results` → прямой API
|
|
||||||
- **`GET /api/log`** — последние 200 строк из `/tmp/app-autotest.log`
|
|
||||||
|
|
||||||
### Логирование
|
|
||||||
|
|
||||||
`_log(msg)` в `api_test.py`:
|
|
||||||
- stdout (gunicorn) + `/tmp/app-autotest.log` (flock, общий для воркеров)
|
|
||||||
- Ротация при 512 КБ
|
|
||||||
- UI: скрытая панель, кнопка `log` (правый нижний угол)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Фронтенд
|
|
||||||
|
|
||||||
### Структура
|
|
||||||
|
|
||||||
Один HTML-файл, три колонки:
|
|
||||||
- Левая (280px): инфраструктура
|
|
||||||
- Средняя (240px): сервисы
|
|
||||||
- Правая (flex): инстансы + параметры + кнопка + этапы
|
|
||||||
|
|
||||||
### Глобальное состояние (JS)
|
|
||||||
|
|
||||||
```
|
|
||||||
svcInstances — кеш инстансов (из /api/operations)
|
|
||||||
selectedInst — UID выбранного инстанса
|
|
||||||
selectedOp — {opId, opName, svcId}
|
|
||||||
pollTimer — таймер поллинга
|
|
||||||
SVC_ID = 1 — фиксированный сервис (Болванка)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Потоки операций
|
|
||||||
|
|
||||||
**CREATE**: `startCreate()` → `showParams(18,'create')` → форма → `executeOp(pp)` → POST /api/test → поллинг → refreshInstances
|
|
||||||
|
|
||||||
**MODIFY**: `toggleInstance()` → кнопки → `runOp('modify',opId)` → `showParams(opId,'modify')` → GET /api/params?instanceUid= → форма с ТЕКУЩИМИ значениями → валидация map → `executeOp(pp)` → POST /api/test → поллинг
|
|
||||||
|
|
||||||
**Без параметров** (suspend/delete/resume/redeploy): `runOp()` → confirm → `executeOp({})` → POST /api/test → поллинг
|
|
||||||
|
|
||||||
### Валидация JSON
|
|
||||||
|
|
||||||
- `_esc(s)` — HTML-экранирование (`"` → `"`, `&` → `&`, `<` → `<`)
|
|
||||||
- `validateJson(el, quiet)` — `JSON.parse()` на `onblur` (красная рамка + текст)
|
|
||||||
- Batch-проверка перед отправкой — ошибка → запрос не уходит
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Nubes API — используемые endpoint'ы
|
|
||||||
|
|
||||||
| Метод | Путь | Назначение |
|
|
||||||
|-------|------|-----------|
|
|
||||||
| GET | `/instances?pageSize=200&page=N` | Все инстансы (пагинация) |
|
|
||||||
| GET | `/instances/{uid}` | Детали + state.params |
|
|
||||||
| GET | `/services` | Список сервисов |
|
|
||||||
| GET | `/services/{id}` | Детали + операции |
|
|
||||||
| GET | `/instanceOperations/default/{opId}` | Шаблон параметров |
|
|
||||||
| POST | `/instances` | Создать инстанс |
|
|
||||||
| POST | `/instanceOperations` | Создать операцию |
|
|
||||||
| POST | `/instanceOperationCfsParams` | Установить параметр |
|
|
||||||
| POST | `/instanceOperations/{opUid}/run` | Запустить операцию |
|
|
||||||
| GET | `/instanceOperations/{opUid}?fields=...` | Поллинг статуса |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Ограничения платформы
|
|
||||||
|
|
||||||
- `site/` — НЕ пакет (без `__init__.py`), импорты без префикса `site.`
|
|
||||||
- `app.run(host="0.0.0.0", port=5000)`
|
|
||||||
- Gunicorn multi-worker → общие данные через файлы + flock
|
|
||||||
- `/tmp/` теряется при редеплое
|
|
||||||
- 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 или общая таблица в БД для статусов операций.
|
|
||||||
-478
@@ -1,478 +0,0 @@
|
|||||||
# История разработки app-autotest
|
|
||||||
|
|
||||||
## v1.1.2 (28.07.2026) — refSvcId: выпадающие списки для cross-service параметров
|
|
||||||
|
|
||||||
- `get_params.py` + `api_test.py`: параметры включают `refSvcId` из API
|
|
||||||
- Фронт: `refSvcId` → `fetch('/api/instances/list')` → фильтр по `serviceId` → `<select>` с `displayName`
|
|
||||||
- Универсально: S3, K8s-кластер, любой сервис — data-driven
|
|
||||||
|
|
||||||
## Инфраструктура
|
|
||||||
- **Кластер**: `iot-naeel` (resourceRealm K8s)
|
|
||||||
- **URL**: `https://atest.pythonk8s.dev.nubes.ru`
|
|
||||||
- **Деплой**: git push → managed service pythonk8s → gunicorn
|
|
||||||
- **PostgreSQL**: `foriot` (509145c3-...), PG 17, `iot-naeel`
|
|
||||||
|
|
||||||
## v1.0.93 (28.07.2026) — отдельные DB_* переменные
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- `db/pool.py`: `_dsn()` собирает DSN из отдельных переменных: DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, DB_SSLMODE
|
|
||||||
- `db/init_db.py`: проверка `DB_USER` вместо `DATABASE_URL`
|
|
||||||
- `secrets/pg-foriot.md`: полный jsonEnv для pythonk8s (6 DB-переменных + 2 существующие)
|
|
||||||
- `app.py`: NUBES_API_ENDPOINT по умолчанию TEST
|
|
||||||
|
|
||||||
## v1.0.92 (27.07.2026) — Steps 5-9: динамические сервисы + PostgreSQL история
|
|
||||||
|
|
||||||
### Шаг 5: JS → /static/app.js
|
|
||||||
- JS вынесен из index.html в отдельный файл
|
|
||||||
- `window.APP = {version, stand, hasUserToken}` — передача Jinja2-переменных
|
|
||||||
|
|
||||||
### Шаг 6: Динамические сервисы
|
|
||||||
- `currentSvcId` вместо хардкода `SVC_ID=1`
|
|
||||||
- `initServices()` — загрузка из `/api/services` при старте
|
|
||||||
|
|
||||||
### Шаг 7-8: PostgreSQL
|
|
||||||
- `db/pool.py` — ThreadedConnectionPool (lazy-init, 1-5 соединений)
|
|
||||||
- `db/init_db.py` — таблица `runs` с индексами (идемпотентно)
|
|
||||||
- `db/save_run.py` — запись результатов в БД
|
|
||||||
- `requirements.txt` — добавлен `psycopg2-binary`
|
|
||||||
|
|
||||||
### Шаг 9: /api/history
|
|
||||||
- GET `/api/history` — последние 50 записей (фильтр по client_id + stand)
|
|
||||||
|
|
||||||
## v1.0.90 (27.07.2026) — Steps 1-4: auth.py + TTL + cookie
|
|
||||||
|
|
||||||
### Шаг 1-2: api/auth.py
|
|
||||||
- Новый модуль: `get_token()`, `get_client()`, `get_client_id()`, `get_stand()`, `get_token_info()`, `get_token_masked()`
|
|
||||||
- Убраны дубликаты из main.py и api_test.py
|
|
||||||
|
|
||||||
### Шаг 3: Cookie httponly + samesite
|
|
||||||
- `set_cookie(..., httponly=True, samesite="Strict")`
|
|
||||||
|
|
||||||
### Шаг 4: _op_results TTL
|
|
||||||
- Максимум 500 записей, удаление старше 1 часа
|
|
||||||
- `_ts` timestamp в каждой записи
|
|
||||||
|
|
||||||
## v1.0.99 (28.07.2026) — фикс: Flask-прокси в фоновом потоке
|
|
||||||
|
|
||||||
### Найденные ошибки
|
|
||||||
1. `get_token_info()` → `request.cookies` → в потоке нет request-контекста → RuntimeError
|
|
||||||
2. `current_app.config.get("VERSION")` → Flask-proxy → та же проблема
|
|
||||||
3. `except: pass` глушил ошибки → записи молча не сохранялись
|
|
||||||
|
|
||||||
### Исправление
|
|
||||||
- `user_email` и `app_version` получаются в `api_test()` (где есть request)
|
|
||||||
и передаются параметрами в `_finish_op` → `save_run`
|
|
||||||
- Добавлено логирование ошибок в `save_run` (больше не глухое `pass`)
|
|
||||||
|
|
||||||
### Урок
|
|
||||||
`py_compile` проверяет только синтаксис. Flask-прокси (`request`, `current_app`, `g`, `session`)
|
|
||||||
**не работают** в фоновых потоках. Все нужные значения — до `threading.Thread` параметрами.
|
|
||||||
|
|
||||||
## v1.0.89 (27.07.2026) — Sonnet Rounds 1-3 + документирование
|
|
||||||
|
|
||||||
### 1. PostgreSQL connection string
|
|
||||||
- `os.getenv("DATABASE_URL")` — никакого service discovery
|
|
||||||
- Инжектится через `jsonEnv` параметры pythonk8s-сервиса (как NUBES_API_TOKEN)
|
|
||||||
- Порядок: создать PostgreSQL-инстанс в Nubes → взять host/port/user/pass из state_out → собрать DSN → jsonEnv → деплой
|
|
||||||
|
|
||||||
### 2. Миграции при деплое
|
|
||||||
- `init_db()` на уровне модуля в app.py, до register_blueprint
|
|
||||||
- `CREATE TABLE/INDEX IF NOT EXISTS` — идемпотентно, каждый воркер выполнит независимо
|
|
||||||
- PostgreSQL обрабатывает конкурентный DDL корректно
|
|
||||||
- Никаких ручных шагов при деплое
|
|
||||||
- `DROP COLUMN` — не идемпотентно, осторожно
|
|
||||||
|
|
||||||
### 3. Multi-service UI (map-fixed)
|
|
||||||
- `dataDescriptor` уже в API-ответе — инфраструктура готова
|
|
||||||
- `map` (свободный): textarea + JSON-валидация (как сейчас)
|
|
||||||
- `map-fixed`: таблица подполей с отдельными input/select из dataDescriptor
|
|
||||||
- Бэкенд не меняется — paramValue всегда строка
|
|
||||||
- Изменения только во фронте: showParams() + сбор подполей в JSON перед отправкой
|
|
||||||
|
|
||||||
### 4. Тесты
|
|
||||||
- pytest для чистых функций без моков: `_find_uid`, `_uid_from_location`, `_with_prefix`, слияние params, tracker
|
|
||||||
- Структура: `tests/conftest.py`, `test_utils.py`, `test_tracker.py`, `test_get_params.py`
|
|
||||||
- Роуты с Flask test client + mock HttpClient — отложить (высокая стоимость мокирования)
|
|
||||||
|
|
||||||
## v1.0.89 (27.07.2026) — Sonnet Round 2: PostgreSQL, auth.py, детальный план
|
|
||||||
|
|
||||||
### Результаты анализа
|
|
||||||
Sonnet дал конкретные ответы на все 7 вопросов Round 2:
|
|
||||||
|
|
||||||
**1. PostgreSQL схема:**
|
|
||||||
- Таблица `runs` с JSONB-полями `params` и `stages`
|
|
||||||
- 3 индекса: `(client_id, stand, created_at DESC)`, `(instance_uid)`, `(created_at)`
|
|
||||||
- Stages в JSONB (не отдельная таблица), instances не нужны (API — source of truth)
|
|
||||||
- Чувствительные params маскировать при сохранении
|
|
||||||
|
|
||||||
**2. Connection pool:**
|
|
||||||
- `psycopg2.pool.ThreadedConnectionPool` — минимум зависимостей
|
|
||||||
- Lazy-init в `db/pool.py` — безопасно для fork-модели gunicorn
|
|
||||||
- `flask.g` + `@app.teardown_appcontext` для возврата соединений
|
|
||||||
|
|
||||||
**3. Миграции:**
|
|
||||||
- `db/init_db.py` с `IF NOT EXISTS` — идемпотентно, авто-применение при старте
|
|
||||||
- Alembic — когда 3+ таблицы или нужен rollback (сейчас не нужно)
|
|
||||||
|
|
||||||
**4. Трекер vs PostgreSQL:**
|
|
||||||
- **НЕ заменять** трекер на БД — это разные слои
|
|
||||||
- Трекер = кеш (секунды), PostgreSQL = история (недели)
|
|
||||||
- Сетевое обращение к БД на критическом пути CREATE недопустимо
|
|
||||||
|
|
||||||
**5. auth.py:**
|
|
||||||
- Явные функции вместо `before_request` — проще, нет скрытых зависимостей
|
|
||||||
- Одна реализация `get_client_id()` вместо двух копий
|
|
||||||
- Кеширование `token → endpoint` (опционально, TTL 5 мин)
|
|
||||||
|
|
||||||
**6. Динамические сервисы:**
|
|
||||||
- `/api/services` из API → фильтр по `enabled: true` из config.yaml
|
|
||||||
- UI: заменить хардкод `SVC_ID=1` на динамическую загрузку в `selectService()`
|
|
||||||
|
|
||||||
**7. JS в файл:**
|
|
||||||
- Классический скрипт, один файл `app.js` (300 строк — не нужно ES modules)
|
|
||||||
- `window.APP = {...}` через `tojson` фильтр Jinja2 для передачи переменных
|
|
||||||
|
|
||||||
### Итоговый порядок реализации (9 шагов)
|
|
||||||
1. `api/auth.py` — вынести get_token/get_client/get_client_id/get_stand
|
|
||||||
2. Заменить дублирующиеся функции в main.py и api_test.py
|
|
||||||
3. Cookie `httponly=True, samesite='Strict'`
|
|
||||||
4. `_op_results` TTL/очистка
|
|
||||||
5. JS вынести в `/static/app.js` + `window.APP`
|
|
||||||
6. Динамические сервисы из `/api/services`
|
|
||||||
7. `db/pool.py` + `db/init_db.py` (PostgreSQL)
|
|
||||||
8. Запись истории в runs при завершении операции
|
|
||||||
9. `/api/history` эндпоинт + UI
|
|
||||||
|
|
||||||
## v1.0.89 (27.07.2026) — Sonnet Round 1: архитектурный аудит
|
|
||||||
- Cloud-first подход с tracker-fallback
|
|
||||||
- HTML-экранирование в JS
|
|
||||||
|
|
||||||
**🔴 Критические находки:**
|
|
||||||
1. `_op_results` dict растёт бесконечно — утечка памяти, нужен TTL/очистка
|
|
||||||
2. Cookie токена без `httponly` и `samesite`
|
|
||||||
3. `/api/log` без проверки авторизации
|
|
||||||
|
|
||||||
**🟡 Дублирование:**
|
|
||||||
1. `_client_id()` — идентичный код в main.py и api_test.py
|
|
||||||
2. `_client()` — разное поведение в main.py и api_test.py
|
|
||||||
3. Инлайн `<style>` дублирует `style.css`
|
|
||||||
|
|
||||||
**🟡 Захардкодено:**
|
|
||||||
1. `SVC_ID=1` и список сервисов в HTML — хотя `/api/services` существует
|
|
||||||
2. `create_client()` в main.py vs `_client()` в api_test.py — разные подходы
|
|
||||||
|
|
||||||
**Приоритетный план исправлений:**
|
|
||||||
1. 🔴 `_op_results` — добавить очистку/TTL
|
|
||||||
2. 🔴 Cookie `httponly=True, samesite='Strict'`
|
|
||||||
3. 🟡 Вынести `_client_id()` / `get_token()` в общий модуль `api/auth.py`
|
|
||||||
4. 🟡 Загрузка сервисов из `/api/services` вместо хардкода
|
|
||||||
5. 🟡 Вынести JS в `/static/app.js`
|
|
||||||
6. 🟢 SQLite история запусков
|
|
||||||
7. 🟢 `/api/log` проверка токена
|
|
||||||
|
|
||||||
### Создан запрос Round 2
|
|
||||||
Файл: `DOCS/sonnet-architecture-review-v1.0.89-r2.md` — уточняющие вопросы по реализации.
|
|
||||||
|
|
||||||
## v1.0.89 (27.07.2026) — документация и комментарии кода
|
|
||||||
|
|
||||||
## v1.0.87 (27.07.2026) — документация: ARCHITECTURE.md переписан, legacy-доки помечены
|
|
||||||
|
|
||||||
## v1.0.84 (27.07.2026) — лог-панель скрыта по умолчанию
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- Лог-панель теперь `display:none`, показывается кнопкой `log` (правый нижний угол).
|
|
||||||
- При скрытой панели `/api/log` не поллится.
|
|
||||||
|
|
||||||
## v1.0.83 (27.07.2026) — HTML-escape + JSON-валидация map-полей
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- `_esc(s)` — HTML-экранирование значений (`"` → `"`, `&` → `&`, `<` → `<`).
|
|
||||||
- `validateJson(el, quiet)` — проверка `JSON.parse()` при `onblur` (красная рамка + текст ошибки).
|
|
||||||
- Batch-проверка всех map-полей перед отправкой — ошибка → запрос не уходит.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Поломку HTML при значениях с кавычками (напр. `{"f":1}`).
|
|
||||||
- Отправку битого JSON в API.
|
|
||||||
|
|
||||||
## v1.0.82 (27.07.2026) — displayName из API для non-create операций
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- `_get_instance_display_name(client, uid)` — `GET /instances/{uid}` → `displayName`.
|
|
||||||
- Используется в не-create ветке `POST /api/test` вместо `instance_uid` как fallback.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- UUID вместо имени инстанса в финальном статусе.
|
|
||||||
|
|
||||||
## v1.0.81 (27.07.2026) — get_params.py: текущие значения из state.params
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- Новый файл `site/operations/get_params.py` — независимый модуль.
|
|
||||||
- `get_params_with_current_values(client, op_id, instance_uid)`:
|
|
||||||
- `GET /instances/{uid}` → `state.params` (текущие значения)
|
|
||||||
- `GET /instanceOperations/default/{opId}` → шаблон
|
|
||||||
- Слияние: `defaultValue = state.params["код"] ?? template.defaultValue`
|
|
||||||
- Убран `previewOpUid` полностью (и из бэкенда, и из фронтенда).
|
|
||||||
|
|
||||||
## v1.0.80 (27.07.2026) — попытка fix через svcOperationId (НЕ СРАБОТАЛО)
|
|
||||||
|
|
||||||
## v1.0.79 (27.07.2026) — файловый лог для multi-worker
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- `_log()` пишет в `/tmp/app-autotest.log` под `fcntl.flock` (вместо in-memory deque).
|
|
||||||
- `/api/log` читает из файла последние 200 строк.
|
|
||||||
- Ротация при 512 КБ.
|
|
||||||
- UI-панель логов (180px, автоскролл, поллинг 2s).
|
|
||||||
|
|
||||||
## v1.0.77 (27.07.2026) — debug-логирование в api_params
|
|
||||||
|
|
||||||
## v1.0.76 (27.07.2026) — modify: preview-операция для получения paramValue (НЕ СРАБОТАЛО)
|
|
||||||
|
|
||||||
# v1.0.74 (27.07.2026) — version bump for push
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` create-операция отделена от обычных операций: CREATE использует фиксированный префикс `autotest-`, а non-create берёт имя выбранного инстанса.
|
|
||||||
- Из non-create ветки убран случайный fallback `tut` и общий create-style `displayName`.
|
|
||||||
- В `site/routes/api_test.py` backend больше не подставляет create-имя по умолчанию для обычных операций и возвращает `displayName` для финального статуса.
|
|
||||||
- В `site/routes/main.py` список autotest-инстансов теперь строится cloud-first без старой склейки через map по имени; tracker остался только как временный fallback, если cloud ещё не вернул новый инстанс.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Путаницу между CREATE и обычными операциями.
|
|
||||||
- Ситуацию, когда в UI появлялся лишний `tut`.
|
|
||||||
- Дубли `running/creating` на одном autotest-инстансе.
|
|
||||||
- Неясный финальный статус, где было видно операцию, но не было понятно, над каким инстансом она выполнялась.
|
|
||||||
|
|
||||||
# v1.0.72 (27.07.2026) — service list keeps one autotest row per displayName
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` сервисный список теперь cloud-first и не склеивает разные строки в одну map.
|
|
||||||
- Tracker остаётся только как fallback, если cloud ещё не вернул конкретный autotest-инстанс.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Дубли `running/creating` на одном и том же autotest-инстансе.
|
|
||||||
|
|
||||||
# v1.0.71 (27.07.2026) — finished status shows instance name
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` финальная строка после `OK/FAIL` теперь показывает имя инстанса.
|
|
||||||
- Имя операции и длительность остаются рядом, отдельно от имени инстанса.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Ситуацию, когда после завершения delete/modify было видно только название операции, но не ясно, над каким инстансом она выполнялась.
|
|
||||||
|
|
||||||
# v1.0.70 (27.07.2026) — dedupe autotest instances by displayName
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` сервисный список теперь склеивает записи по `displayName`.
|
|
||||||
- Если cloud уже отдал `running`, он выигрывает у tracker-fallback `creating`.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Дубли одной и той же autotest-записи после CREATE, когда в списке появлялись и `running`, и `creating`.
|
|
||||||
|
|
||||||
## v1.0.69 (27.07.2026) — autotest prefix fixed in CREATE field
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` префикс `autotest-` отображается отдельным фиксированным текстом.
|
|
||||||
- Рядом с ним остаётся только редактируемый суффикс имени.
|
|
||||||
- При отправке CREATE имя собирается как `autotest-` + введённый хвост.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Ситуацию, когда весь `displayName` показывался в одном редактируемом поле.
|
|
||||||
- Риск случайно стереть обязательный префикс `autotest-`.
|
|
||||||
|
|
||||||
## v1.0.68 (27.07.2026) — operation name shown after OK + muted operation buttons
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` финальный статус теперь показывает имя операции рядом со временем.
|
|
||||||
- Кнопки операций получили спокойные неброские оттенки по типу операции.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Неясность после завершения операции, когда было видно только `OK` и время без указания, что именно выполнялось.
|
|
||||||
- Слишком кислотный вид кнопок операций.
|
|
||||||
|
|
||||||
## v1.0.67 (27.07.2026) — autotest instances are shown by cloud prefix
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` список инстансов сервиса больше не отфильтровывается через `/tmp/instances.json`.
|
|
||||||
- Теперь в таблицу попадают все облачные инстансы с префиксом `autotest-`.
|
|
||||||
- Трекер оставлен только как fallback, если новый autotest-инстанс ещё не успел появиться в ответе облака.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Ситуацию, когда autotest-инстанс уже есть в облаке, но не показывается в таблице из-за отсутствия записи в json.
|
|
||||||
|
|
||||||
## v1.0.66 (27.07.2026) — backend status resolver for operations
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` добавлен канонический резолвер статуса инстанса для `/api/operations/<svc_id>`.
|
|
||||||
- Backend теперь отдаёт `status` для инстансов, а UI читает именно его вместо угадывания по `explainedStatus`.
|
|
||||||
- В `site/templates/index.html` убрано раннее отображение «Готово» во время выполнения операции.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Путаницу между `creating`, `running` и фактическим статусом в UI.
|
|
||||||
- Ситуацию, когда фронтенд сам интерпретировал статусы и показывал не то состояние.
|
|
||||||
|
|
||||||
## v1.0.65 (27.07.2026) — cloud list pagination fix
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/operations/get_instances.py` пагинация списка инстансов теперь останавливается по размеру страницы, а не по `total`.
|
|
||||||
- Это убирает ситуацию, когда инстанс уже создан в облаке, но приложение не дочитало следующую страницу и показывает `creating`.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` и `site/app.py` синхронизирована версия `1.0.65`.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Ложный `creating` для уже существующего инстанса, если он попал не на первую страницу `/instances`.
|
|
||||||
|
|
||||||
## v1.0.64 (27.07.2026) — header version + editable displayName placeholder
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` версия теперь гарантированно попадает в `config.VERSION` даже после `load_config()`.
|
|
||||||
- В `site/templates/index.html` поле `displayName` остаётся редактируемым, но автотест-имя показывается как placeholder.
|
|
||||||
- При CREATE пользователь может ввести своё имя, а backend всё равно добавит `autotest-` и проверит уникальность.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` зафиксированы оба изменения.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Пропадающую версию в шапке.
|
|
||||||
- Неправильное поведение поля `displayName`, когда оно выглядело как готовое значение вместо подсказки.
|
|
||||||
|
|
||||||
## v1.0.63 (27.07.2026) — autotest namespace for instances
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` CREATE теперь генерирует `displayName` с префиксом `autotest-` и проверяет его на уникальность среди текущих инстансов.
|
|
||||||
- В `site/routes/api_test.py` backend тоже нормализует `displayName` с этим префиксом и, если нужно, добавляет суффикс для уникальности перед созданием в облаке.
|
|
||||||
- В `site/routes/main.py` список инстансов фильтруется по namespace `autotest-`, чтобы в UI попадали только тестовые инстансы приложения.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` зафиксирован переход на namespace-based фильтрацию.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Дубли `displayName` при CREATE.
|
|
||||||
- Чужие инстансы в списке.
|
|
||||||
- Зависимость от локального трекера как от основного признака принадлежности.
|
|
||||||
|
|
||||||
## v1.0.62 (27.07.2026) — immediate visual refresh after CREATE
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` после завершения CREATE список инстансов перерисовывается полностью.
|
|
||||||
- `refreshInstances()` больше не зависит от DOM-diff и сразу ставит новый инстанс в список.
|
|
||||||
- `DOCS/ARCHITECTURE.md` и `site/app.py` синхронизированы на `1.0.62`.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- После появления кнопки «Готово» созданный инстанс должен сразу быть виден в списке.
|
|
||||||
|
|
||||||
## v1.0.61 (27.07.2026) — immediate list rebuild after CREATE
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` `refreshInstances()` теперь всегда полностью перерисовывает список инстансов после завершения операции.
|
|
||||||
- После `OK` новый инстанс должен появляться в списке сразу, без зависимости от DOM-diff и без перезагрузки страницы.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` и `site/app.py` синхронизирована версия `1.0.61`.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Убрана ситуация, когда после CREATE статус уже `OK`, но в UI инстанс ещё не виден.
|
|
||||||
|
|
||||||
## v1.0.60 (27.07.2026) — full list rerender after CREATE
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` `refreshInstances()` теперь полностью перерисовывает список инстансов, а не пытается вставить только diff.
|
|
||||||
- После завершения CREATE новый инстанс должен появляться в списке сразу после `OK`.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` добавлен отдельный шаг про полную перерисовку списка.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Снята зависимость от частичного DOM-diff-а, из-за которого созданный инстанс мог не появляться сразу.
|
|
||||||
|
|
||||||
## v1.0.59 (27.07.2026) — restore tracked instance after reload
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` tracked-инстансы, которых Nubes уже не отдает в ответе после reload, снова добавляются в список из `/tmp/instances.json`.
|
|
||||||
- В `site/templates/index.html` статус CREATE теперь показывает имя инстанса, а не только `create`.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` зафиксирован новый шаг восстановления списка после F5.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- После F5 инстанс не пропадает из UI, если он есть в локальном трекере.
|
|
||||||
- В статусе выполнения видно, по какому displayName идет CREATE.
|
|
||||||
|
|
||||||
## v1.0.58 (27.07.2026) — version bump
|
|
||||||
|
|
||||||
- Поднята версия проекта до `1.0.58`.
|
|
||||||
- Синхронизированы заголовки в `site/app.py` и `DOCS/ARCHITECTURE.md`.
|
|
||||||
|
|
||||||
## v1.0.57 (27.07.2026) — UI hardening + history sync
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` финальное состояние кнопки вынесено в `setFinishedState()`, чтобы не держать логику завершения операции в двух местах.
|
|
||||||
- После завершения операции кнопка «Готово» теперь гарантированно сбрасывает обработчик через `btn.onclick = null`.
|
|
||||||
- Повторный CREATE через старый обработчик больше не воспроизводится.
|
|
||||||
- `displayName` больше не теряется в нормальном сценарии завершения CREATE.
|
|
||||||
- В `site/app.py` версия поднята до `1.0.57`.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` отмечено закрытие бага с повторным CREATE и синхронизирован статус по `displayName`.
|
|
||||||
|
|
||||||
### Зачем это было сделано
|
|
||||||
- Убрать хрупкость вокруг состояния кнопки после завершения операции.
|
|
||||||
- Привести документацию и версию к текущему состоянию кода.
|
|
||||||
|
|
||||||
## v1.0.54 (27.07.2026) — Аудит #3: 4 бага исправлено
|
|
||||||
|
|
||||||
### Баг #1: `_client()` не автоопределял стенд (КРИТИЧЕСКИЙ)
|
|
||||||
- **Где:** `api_test.py:_client()` — использовал `current_app.config["NUBES_API_ENDPOINT"]` (raw env var)
|
|
||||||
- **Симптом:** main.py автоопределял стенд для главной, но POST /api/test падал с 401 если токен от другого стенда
|
|
||||||
- **Исправление:** `detect_endpoint()` вынесен в `api/http_client.py`, используется в `_client()`
|
|
||||||
- **Как пропустили:** анализировал main.py и api_test.py в изоляции, не сравнил
|
|
||||||
|
|
||||||
### Баг #2: `_find_uid()` возвращал не тот UUID (КРИТИЧЕСКИЙ)
|
|
||||||
- **Где:** `api_test.py:_find_uid()` — итерировал по ВСЕМ значениям dict, искал 36-символьную строку
|
|
||||||
- **Симптом:** `/instanceOperations` возвращает оба `instanceUid` + `instanceOperationUid`, функция брала первый попавшийся → `run` шёл на неправильный URL
|
|
||||||
- **Исправление:** ищет по конкретным ключам: `instanceOperationUid` → `instanceUid` → `uid` → `Uid`
|
|
||||||
- **Как пропустили:** не проверил реальный формат ответа API
|
|
||||||
|
|
||||||
### Баг #3: Сироты при ошибке params (СРЕДНИЙ)
|
|
||||||
- **Где:** `api_test.py`: порядок — `POST /instances` → `POST /instanceOperations` → params loop → `tracker_add`
|
|
||||||
- **Симптом:** если params падает — инстанс и операция созданы в Nubes, трекер пуст → сироты
|
|
||||||
- **Исправление:** `tracker_add` вызывается ДО params loop, сразу после получения `instanceUid`
|
|
||||||
- **Как пропустили:** не трассировал линейно порядок вызовов
|
|
||||||
|
|
||||||
### Баг #4: `flock` без таймаута (НИЗКИЙ)
|
|
||||||
- **Где:** `tracker.py:_locked_read/_write` — `fcntl.flock(fd, LOCK_EX)` без LOCK_NB
|
|
||||||
- **Симптом:** при зависшем процессе с локом все воркеры блокируются навсегда
|
|
||||||
- **Исправление:** `_acquire_lock()` с `LOCK_EX | LOCK_NB` + retry до 2 секунд
|
|
||||||
- **Как пропустили:** новый код flock не перечитал с нуля после добавления
|
|
||||||
|
|
||||||
### Архитектурное: `detect_endpoint` вынесен в http_client.py
|
|
||||||
- Раньше был в main.py → недоступен для api_test.py
|
|
||||||
- Теперь в `api/http_client.py` → оба модуля используют
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.0.53 (27.07.2026) — Файловый трекер с fcntl.flock
|
|
||||||
- In-memory dict заменён на `/tmp/instances.json` с `fcntl.LOCK_EX`
|
|
||||||
- Multi-worker gunicorn: все воркеры читают/пишут один файл под локом
|
|
||||||
|
|
||||||
## v1.0.52 (27.07.2026) — `_tmpl()` helper
|
|
||||||
- Все переменные шаблона передаются во всех трёх return-путях index()
|
|
||||||
- Исправлен UndefinedError при «Выйти»
|
|
||||||
|
|
||||||
## v1.0.51 (27.07.2026) — Автоопределение стенда + UnboundLocalError
|
|
||||||
- `detect_endpoint()` в main.py: пробует токен против dev и test API
|
|
||||||
- `instance_groups = {}` инициализирован до if
|
|
||||||
- `_finish_op` обёрнут в полный try/except
|
|
||||||
|
|
||||||
## v1.0.50 (27.07.2026) — showStages + api_operations fix
|
|
||||||
- `showStages()`: dtFinish вместо isSuccessful для ⏳/✅/❌
|
|
||||||
- `api_operations()`: не фильтровать "not created", добавлять tracked-сирот
|
|
||||||
|
|
||||||
## v1.0.49 (27.07.2026) — In-memory tracker + refreshInstances
|
|
||||||
- Трекер: in-memory dict вместо файла `/tmp/instances.json`
|
|
||||||
- `refreshInstances()` добавляет новые инстансы в DOM (не только бейджи)
|
|
||||||
- `selectService()` заменён на `refreshInstances()` после OK
|
|
||||||
|
|
||||||
## v1.0.48 (27.07.2026) — tracker_add синхронно
|
|
||||||
- `tracker_add` в `api_test()` до `threading.Thread`
|
|
||||||
- Лог `is_ok` в `_finish_op`
|
|
||||||
- Flexbox-кнопки горизонтально
|
|
||||||
|
|
||||||
## v1.0.47 (27.07.2026) — displayName fix
|
|
||||||
- displayName захватывается до очистки формы
|
|
||||||
- tracker_add до _op_results[OK]
|
|
||||||
- autotest-1 в _INITIAL
|
|
||||||
|
|
||||||
## v1.0.46 (27.07.2026) — _finish_op signature
|
|
||||||
- Добавлены op_name, svc_op_id в _finish_op()
|
|
||||||
|
|
||||||
## v1.0.45 и ранее
|
|
||||||
- Базовый CREATE/MODIFY/SUSPEND/DELETE/RESUME/REDEPLOY
|
|
||||||
- UI с этапами, поллинг, params форма
|
|
||||||
- Множественные баги CREATE flow (документированы в DOCS/api-create-flow.md)
|
|
||||||
-103
@@ -1,103 +0,0 @@
|
|||||||
# 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 (тестовый стенд) |
|
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
# Запрос на полный анализ autotest — для нового чата с агентом
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
Версия: v1.1.48
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. ЧТО ЭТО ЗА ПРОЕКТ
|
|
||||||
|
|
||||||
**autotest** — веб-приложение для автоматизированного тестирования сервисов платформы Nubes (Cloud Director). Позволяет запускать операции (create/modify/delete/suspend/resume/redeploy) над инстансами сервисов через Nubes REST API, отслеживать статус, вести историю запусков.
|
|
||||||
|
|
||||||
Приложение запущено на тестовом стенде: `atest.pythonk8s.dev.nubes.ru`
|
|
||||||
|
|
||||||
**Стек:**
|
|
||||||
- Backend: Flask 3.0 + gunicorn (multi-worker)
|
|
||||||
- DB: PostgreSQL 17 (Zalando Operator), схемы: `runs`, `scenario_runs`, `scenario_definitions` (pending)
|
|
||||||
- Frontend: ванильный JS (один файл `app.js`, 550 строк), Jinja2-шаблон
|
|
||||||
- Nubes REST API: `https://lk-api-gateway-test.ngcloud.ru/api/v1/svc`
|
|
||||||
- Аутентификация: JWT-токен (cookie или env), автоопределение стенда (dev/test)
|
|
||||||
- Деплой: Nubes pythonk8s, кластер iot-naeel
|
|
||||||
|
|
||||||
**Автор:** naeel (tazetdinovn@gmail.com, WZ03709)
|
|
||||||
**Репозитории:**
|
|
||||||
- `https://gitea.services.ngcloud.ru/forcloud/app-autotest` — код приложения
|
|
||||||
- `https://gitea.services.ngcloud.ru/forcloud/autotest` — документация, YAML-описания сервисов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. АРХИТЕКТУРА — ФАЙЛЫ И ИХ РОЛИ
|
|
||||||
|
|
||||||
### site/app.py — точка входа
|
|
||||||
Flask-приложение, регистрирует blueprint'ы:
|
|
||||||
- `main_bp` — главная страница, токен, инфраструктура
|
|
||||||
- `api_test_bp` — основной API (run/params/status/log/history)
|
|
||||||
- `api_scenario_bp` — API сценариев (run/status/CRUD — pending)
|
|
||||||
- `api_bp` — **УДАЛЁН в v1.1.45** (legacy: /api/run, /api/status, /api/config)
|
|
||||||
- VERSION — меняется при КАЖДОМ изменении
|
|
||||||
|
|
||||||
### site/api/http_client.py — HTTP-клиент
|
|
||||||
- `HttpClient` — обёртка над requests.Session
|
|
||||||
- GET: `raise_for_status()` → `.json()`, пустой ответ → `{}`
|
|
||||||
- POST: `json=data`, извлекает Location-заголовок → `_location` в ответе
|
|
||||||
- `raw_delete(url)` — DELETE без auth (для CMDB API)
|
|
||||||
- `detect_endpoint(token)` — автоопределение стенда (dev→test)
|
|
||||||
- `stand_name(endpoint)` — "dev"/"test" по URL
|
|
||||||
|
|
||||||
### site/api/auth.py — аутентификация
|
|
||||||
- `get_token()` — из cookie или env
|
|
||||||
- `get_client()` — HttpClient с автостендом
|
|
||||||
- `get_client_id()` — ClientID из JWT
|
|
||||||
- `get_stand()` — "dev"/"test"
|
|
||||||
- `get_token_info()` — {email, company, client_id}
|
|
||||||
|
|
||||||
### site/routes/main.py — главная страница
|
|
||||||
- `GET/POST /` — Jinja2-рендер: организация, инфраструктура, сервисы, форма токена
|
|
||||||
- `GET /api/operations/<svc_id>` — cloud-first инстансы + tracked-fallback
|
|
||||||
- `_resolve_instance_status()` — explainedStatus из облака или "creating" из трекера
|
|
||||||
|
|
||||||
### site/routes/api_test.py — ОСНОВНОЙ API (400 строк)
|
|
||||||
Эндпоинты:
|
|
||||||
- `GET /api/services` — список сервисов
|
|
||||||
- `GET /api/instances/list` — все инстансы
|
|
||||||
- `GET /api/params/<op_id>[?instanceUid=xxx]` — параметры операции (текущие или шаблон)
|
|
||||||
- `POST /api/test` — запуск операции (CREATE или non-CREATE)
|
|
||||||
- `GET /api/test/status/<op_uid>` — поллинг
|
|
||||||
- `GET /api/log` — логи
|
|
||||||
- `GET /api/history` — история из БД
|
|
||||||
|
|
||||||
Ключевые функции:
|
|
||||||
- `_send_params_terraform()` — шаги 3-7 Terraform (cfsParams → resolveRefSvc → send → validate)
|
|
||||||
- `_normalize_value()` — normalizeUniversalValueV6 (Terraform-equivalent)
|
|
||||||
- `_resolve_ref_svc()` — автоподстановка UUID инстанса для refSvcId-параметров
|
|
||||||
- `_finish_op()` — фоновый поллинг до dtFinish, сохранение в БД
|
|
||||||
- `_redact_params()` — замена secret/password/token на ***
|
|
||||||
- `_unique_display_name()` — проверка на дубликат + суффикс
|
|
||||||
- `_find_uid(resp)` — извлечение UUID из вложенных dict
|
|
||||||
- `_uid_from_location(loc)` — извлечение UUID из Location-заголовка
|
|
||||||
|
|
||||||
### site/routes/api_scenario.py — API СЦЕНАРИЕВ (90 строк)
|
|
||||||
- `GET /api/scenarios` — список из config.yaml (должен быть переключён на БД)
|
|
||||||
- `POST /api/scenario/run` — запуск (создаёт запись ДО потока, возвращает run_id + 202)
|
|
||||||
- `GET /api/scenario/run/<int:run_id>` — статус конкретного запуска
|
|
||||||
- `GET /api/scenario/status` — последние 10 запусков (legacy, должен уйти)
|
|
||||||
|
|
||||||
### site/operations/scenario.py — ЯДРО СЦЕНАРИЕВ (290 строк)
|
|
||||||
- `run_scenario()` — главный исполнитель:
|
|
||||||
1. service имя → svc_id (из config.yaml services)
|
|
||||||
2. operation имя → svcOperationId (GET /services/{id})
|
|
||||||
3. param коды → numeric IDs (GET /instanceOperations/default/{opId})
|
|
||||||
4. create: новый инстанс; остальные: переиспользовать instance_map
|
|
||||||
5. POST /instances → POST /instanceOperations → _send_params_terraform → run → poll
|
|
||||||
6. Сохранить в runs (RUNNING до API, финальный после) + scenario_runs
|
|
||||||
- `_resolve_service()` — поиск svc_id по символическому имени
|
|
||||||
- `_resolve_params()` — symbolic codes → numeric IDs
|
|
||||||
- `_find_uid()`, `_uid_from_location()` — ДУБЛИКАТЫ из api_test.py (!)
|
|
||||||
- `_save_scenario_run()` — UPDATE scenario_runs
|
|
||||||
- `_create_scenario_run()` — INSERT scenario_runs
|
|
||||||
|
|
||||||
### site/operations/get_params.py — параметры с текущими значениями
|
|
||||||
- `get_params_with_current_values()` — смержить state.params инстанса с шаблоном операции
|
|
||||||
- `_normalize_value_list()` — CSV-строка/массив → list
|
|
||||||
|
|
||||||
### site/operations/get_services.py — сервисы
|
|
||||||
- `get_services()` — GET /services → все сервисы
|
|
||||||
- `get_service_detail()` — GET /services/{id} → детали + операции
|
|
||||||
|
|
||||||
### site/operations/get_instances.py — инстансы
|
|
||||||
- `get_organization()` — инстанс с serviceId=19
|
|
||||||
- `get_instances()` — GET /instances?pageSize=500 → все инстансы
|
|
||||||
|
|
||||||
### site/operations/service_list.py — разрешённые сервисы
|
|
||||||
- `load_service_ids(stand)` — чтение `config/services_{stand}.txt`
|
|
||||||
|
|
||||||
### site/operations/tracker.py — трекер инстансов (файловый)
|
|
||||||
- JSON-файлы в /tmp/: `instances-{clientId}-{stand}.json`
|
|
||||||
- fcntl.flock для multi-worker safety
|
|
||||||
- Используется как КРАТКОСРОЧНЫЙ fallback (инстанс создан, но облако ещё не показывает)
|
|
||||||
|
|
||||||
### site/db/pool.py — connection pool
|
|
||||||
- Lazy-init ThreadedConnectionPool (1-5)
|
|
||||||
- `_ensure_schema()` → `init_db()` после успешного коннекта
|
|
||||||
- ENV: DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, DB_SSLMODE
|
|
||||||
|
|
||||||
### site/db/init_db.py — схема БД
|
|
||||||
- Таблица `runs` — история операций (id, client_id, stand, svc_id, op_name, status, duration, params JSONB, stages JSONB, scenario_run_id, step_number)
|
|
||||||
- Таблица `scenario_runs` — запуски сценариев (id, scenario_name, status, current_step, total_steps, duration, error_log)
|
|
||||||
- Миграции: ALTER TABLE ADD COLUMN IF NOT EXISTS
|
|
||||||
- **НЕТ** таблицы `scenario_definitions` — её ещё предстоит создать
|
|
||||||
|
|
||||||
### site/db/save_run.py — сохранение в БД
|
|
||||||
- `save_run()` — INSERT в runs
|
|
||||||
- Принимает опциональные `scenario_run_id` + `step_number` (добавлены в v1.1.48)
|
|
||||||
|
|
||||||
### site/runner.py — LEGACY (будет удалён)
|
|
||||||
- Старый механизм запуска тестов из config.yaml
|
|
||||||
- Используется только `load_config()` из него
|
|
||||||
|
|
||||||
### site/static/app.js — ФРОНТЕНД (550 строк)
|
|
||||||
- Глобальное состояние: svcInstances, selectedInst, selectedOp, pollTimer, currentSvcId, busy, AUTOTEST_PREFIX
|
|
||||||
- selectService(), toggleInstance(), startCreate(), runOp(), showParams(), executeOp()
|
|
||||||
- Поллинг: setFinishedState(), showStages(), stopPoll(), refreshInstances()
|
|
||||||
- История: toggleHistory(), loadHistory()
|
|
||||||
- Сценарии: toggleScenario(), loadScenarios(), runScenario(), stopScenarioPoll()
|
|
||||||
- Хелперы: _esc() (HTML-escape), validateJson()
|
|
||||||
- Логи: toggleLog(), startLogPoll()
|
|
||||||
|
|
||||||
### site/static/style.css — стили
|
|
||||||
### site/templates/index.html — Jinja2-шаблон
|
|
||||||
- Три колонки: инфраструктура | сервисы | инстансы+операции+параметры
|
|
||||||
- Секции: история (сворачиваемая), сценарии (сворачиваемая)
|
|
||||||
- window.APP: version, stand, hasUserToken, firstServiceId
|
|
||||||
|
|
||||||
### site/config.yaml — конфигурация
|
|
||||||
- services: маппинг name → service_id
|
|
||||||
- scenarios: один сценарий dummy_test (create → delete)
|
|
||||||
- **Проблема:** сценарии в контейнере, требуют redeploy для изменения
|
|
||||||
|
|
||||||
### STANDS/dev/resources_yaml/ — YAML-описания сервисов (50+ файлов)
|
|
||||||
- Полные описания: name, service_id, outputs, operations с параметрами (коды, типы, valueList, sub_params)
|
|
||||||
- Используются ТОЛЬКО как документация, код их не читает
|
|
||||||
|
|
||||||
### DOCS/ — документация
|
|
||||||
- ARCHITECTURE-FULL.md — полный обзор
|
|
||||||
- sol-answers.md — 14 ответов Sol (MVP-план)
|
|
||||||
- sol-scenario-editor.md — ответы Sol по редактору сценариев
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. ПОТОК ОПЕРАЦИИ CREATE (Terraform parity)
|
|
||||||
|
|
||||||
1. POST /instances → `instance_uid` из Location-заголовка `./UUID`
|
|
||||||
2. POST /instanceOperations → `op_uid` из Location-заголовка
|
|
||||||
3. GET /instanceOperations/{op_uid}?fields=cfsParams → шаблон параметров
|
|
||||||
4. _resolve_ref_svc → автоподстановка UUID для refSvcId-параметров
|
|
||||||
5. POST /instanceOperationCfsParams → отправить пользовательские параметры
|
|
||||||
6. POST /instanceOperationCfsParams → дослать неотправленные (с normalize)
|
|
||||||
7. GET /instanceOperations/{op_uid}/validate-cfs → валидация
|
|
||||||
8. POST /instanceOperations/{op_uid}/run → запуск
|
|
||||||
9. Поллинг GET /instanceOperations/{op_uid}?fields=dtFinish,... → dtFinish
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. ТЕКУЩЕЕ СОСТОЯНИЕ (v1.1.48)
|
|
||||||
|
|
||||||
### Работает:
|
|
||||||
- ✅ Ручной запуск операций (create/modify/delete/suspend/resume/redeploy)
|
|
||||||
- ✅ Terraform-совместимая отправка параметров (9 шагов)
|
|
||||||
- ✅ Поллинг с этапами (⏳/✅/❌)
|
|
||||||
- ✅ История в БД (runs)
|
|
||||||
- ✅ Лог-панель (fcntl.flock, ротация)
|
|
||||||
- ✅ HTML-escape (statusError, d.error, логи, currentSvcName)
|
|
||||||
- ✅ Redact secrets перед сохранением
|
|
||||||
- ✅ Валидация входов (UUID, int, dict)
|
|
||||||
- ✅ refSvcId — верхнеуровневый + в dataDescriptor
|
|
||||||
- ✅ Убран legacy api_bp
|
|
||||||
|
|
||||||
### Частично работает:
|
|
||||||
- ⚠️ Сценарии — запуск есть, но источник = config.yaml (redeploy для правки)
|
|
||||||
- ⚠️ save_run пишет scenario_run_id + step_number (v1.1.48), но не протестировано
|
|
||||||
- ⚠️ run_id возвращается клиенту (v1.1.48), поллинг по конкретному запуску
|
|
||||||
|
|
||||||
### Не работает / pending:
|
|
||||||
- ❌ Редактор сценариев в UI (нужна таблица scenario_definitions + CRUD API + UI)
|
|
||||||
- ❌ Сценарии требуют redeploy для изменения (запечены в config.yaml)
|
|
||||||
- ❌ Дубликаты _find_uid / _uid_from_location в scenario.py и api_test.py
|
|
||||||
- ❌ scenario.py не протестирован на полный цикл create→delete
|
|
||||||
- ❌ seed из config.yaml в БД при первом старте не реализован
|
|
||||||
- ❌ Нет сервисных токенов (runner использует персональный токен)
|
|
||||||
- ❌ Нет блокировки параллельных запусков сценариев
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. КЛЮЧЕВЫЕ НАХОДКИ ИЗ HAR-ТРАССИРОВКИ
|
|
||||||
|
|
||||||
POST /instances:
|
|
||||||
- Request: `{"serviceId":1,"displayName":"dummy-11255555","descr":""}`
|
|
||||||
- Response: status 201, body `{}`, Location: `./CDEBB216-E5EB-4C09-8736-7E5F01A4EE12`
|
|
||||||
- **UUID ТОЛЬКО в Location-заголовке**, тело ответа — пустой объект `{}`
|
|
||||||
|
|
||||||
POST /instanceOperations:
|
|
||||||
- Request: `{"instanceUid":"...","operation":"create"}`
|
|
||||||
- Response: также Location-заголовок с opUid
|
|
||||||
|
|
||||||
**Следствие:** `_find_uid(resp)` никогда не найдёт UUID в теле ответа (тело пустое). UUID всегда в Location. `_uid_from_location(resp.get("_location",""))` — ЕДИНСТВЕННЫЙ работающий метод извлечения.
|
|
||||||
|
|
||||||
**Но:** `_find_uid` нужен для других ответов API (get instanceOperation), где UUID внутри `instanceOperation.instanceOperationUid`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. РЕКОМЕНДАЦИИ SOL (2026-07-30)
|
|
||||||
|
|
||||||
### По редактору сценариев (sol-scenario-editor.md):
|
|
||||||
|
|
||||||
1. **Структура steps:** JSONB, поле `resource` для связи create→modify→delete одного инстанса
|
|
||||||
2. **Параметры:** выпадающие списки из живого API (сервис→операции→коды), кеш на время сессии
|
|
||||||
3. **Seed:** marker `scenario_seed_v1=completed`, `INSERT ON CONFLICT DO NOTHING`, отдельный `scenario_seed.yaml`
|
|
||||||
4. **UI:** модальное окно, кнопки вверх/вниз, мягкое удаление (is_active=false)
|
|
||||||
5. **Валидация:** при сохранении + при запуске, невалидный не запускать
|
|
||||||
6. **Критические дыры:**
|
|
||||||
- run_id до потока (✅ исправлено v1.1.48)
|
|
||||||
- GET /api/scenario/run/<id> (✅ добавлен v1.1.48)
|
|
||||||
- save_run пишет scenario_run_id/step_number (✅ исправлено v1.1.48)
|
|
||||||
- TIMEOUT: op_data инициализировать (✅ исправлено v1.1.48)
|
|
||||||
- RUNNING до API (✅ исправлено v1.1.48)
|
|
||||||
- Блокировка параллельных запусков (pending)
|
|
||||||
- version в definitions для optimistic locking (pending)
|
|
||||||
- Сервисные токены (pending)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. ЧТО НУЖНО ОТ АГЕНТА
|
|
||||||
|
|
||||||
### Проанализировать и ответить:
|
|
||||||
|
|
||||||
1. **Общая архитектура** — оценить, найти слабые места, предложить улучшения
|
|
||||||
|
|
||||||
2. **Дублирование кода** — `_find_uid` и `_uid_from_location` есть и в api_test.py и в scenario.py. Вынести в общий модуль? Куда?
|
|
||||||
|
|
||||||
3. **scenario_definitions** — спроектировать таблицу:
|
|
||||||
- Поля: id, client_id, stand, name, steps JSONB, version, is_active, created_at, updated_at, updated_by
|
|
||||||
- Индексы: UNIQUE(client_id, stand, lower(name))
|
|
||||||
- API CRUD: какие именно эндпоинты, какие проверки
|
|
||||||
- Seed: как именно импортировать из scenario_seed.yaml при первом старте
|
|
||||||
|
|
||||||
4. **UI редактора сценариев:**
|
|
||||||
- Модальное окно: структура HTML
|
|
||||||
- JS-логика: загрузка списка, создание/редактирование/удаление
|
|
||||||
- Выпадающие списки: сервисы из GET /api/services, операции из GET /api/operations/{svcId}, параметры из GET /api/params/{opId}
|
|
||||||
- Валидация на фронте перед отправкой
|
|
||||||
|
|
||||||
5. **Runner** — нужно ли что-то менять в `run_scenario()`? Он сейчас берёт шаги из config.yaml, должен из БД. Также нужно сохранять snapshot шагов в scenario_runs.
|
|
||||||
|
|
||||||
6. **Безопасность:**
|
|
||||||
- CRUD сценариев: проверка client_id+stand
|
|
||||||
- Optimistic locking (version)
|
|
||||||
- Блокировка параллельных запусков (HTTP 409)
|
|
||||||
|
|
||||||
7. **Что ещё критично упущено?** — любые дыры, которые не покрыты текущим планом
|
|
||||||
|
|
||||||
8. **Приоритетный порядок реализации:**
|
|
||||||
- Что делать сначала, что потом
|
|
||||||
- Что можно выпустить в v1.1.49, что отложить
|
|
||||||
|
|
||||||
### НЕ ДЕЛАТЬ:
|
|
||||||
- Не писать код (только анализ и план)
|
|
||||||
- Не трогать git
|
|
||||||
- Не менять файлы
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. ФАЙЛЫ ДЛЯ АНАЛИЗА (читать в этом порядке)
|
|
||||||
|
|
||||||
1. `app-autotest/site/app.py`
|
|
||||||
2. `app-autotest/site/api/http_client.py`
|
|
||||||
3. `app-autotest/site/api/auth.py`
|
|
||||||
4. `app-autotest/site/routes/main.py`
|
|
||||||
5. `app-autotest/site/routes/api_test.py` (самый важный)
|
|
||||||
6. `app-autotest/site/routes/api_scenario.py`
|
|
||||||
7. `app-autotest/site/operations/scenario.py`
|
|
||||||
8. `app-autotest/site/operations/get_params.py`
|
|
||||||
9. `app-autotest/site/operations/get_services.py`
|
|
||||||
10. `app-autotest/site/operations/get_instances.py`
|
|
||||||
11. `app-autotest/site/operations/tracker.py`
|
|
||||||
12. `app-autotest/site/operations/service_list.py`
|
|
||||||
13. `app-autotest/site/db/pool.py`
|
|
||||||
14. `app-autotest/site/db/init_db.py`
|
|
||||||
15. `app-autotest/site/db/save_run.py`
|
|
||||||
16. `app-autotest/site/runner.py`
|
|
||||||
17. `app-autotest/site/config.yaml`
|
|
||||||
18. `app-autotest/site/static/app.js`
|
|
||||||
19. `app-autotest/site/templates/index.html`
|
|
||||||
20. `app-autotest/site/static/style.css`
|
|
||||||
21. `development/dummycreate.har` (HAR-трассировка CREATE)
|
|
||||||
22. `DOCS/ARCHITECTURE-FULL.md`
|
|
||||||
23. `DOCS/sol-answers.md`
|
|
||||||
24. `DOCS/sol-scenario-editor.md`
|
|
||||||
25. `app-autotest/site/config/services_test.txt`
|
|
||||||
26. Любой YAML из `STANDS/dev/resources_yaml/` (например `1_dummy.yaml`, `115_mariadb.yaml`)
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Архитектура 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
|
|
||||||
- [↑][↓] перестановка шагов
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# GPT-5.6 Sol — План развития autotest MVP
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
|
|
||||||
## 10 шагов
|
|
||||||
|
|
||||||
1. Удалить legacy: api.py, runner.py, CMDB DELETE
|
|
||||||
2. Единый исполнитель операций (operation_executor.py)
|
|
||||||
3. YAML-сценарии (loader + validator)
|
|
||||||
4. Разделение токенов (сервисные vs персональные)
|
|
||||||
5. Prod safety policy (серверные проверки)
|
|
||||||
6. PG как источник состояния (scenario_runs, scenario_steps)
|
|
||||||
7. Runner + API (последовательное выполнение)
|
|
||||||
8. UI: вкладки «Ручные», «Сценарии», «История»
|
|
||||||
9. Аудит: TIMEOUT в БД, refSvc fix, redact secrets, общая история
|
|
||||||
10. Проверка: тесты validator, executor, DB, smoke
|
|
||||||
|
|
||||||
## Мой порядок
|
|
||||||
|
|
||||||
Фаза 1 (база): шаги 1→2→9 (убрать мусор, единый executor, багфиксы)
|
|
||||||
Фаза 2 (ядро): шаги 4→5→6→3 (токены, policy, БД, YAML)
|
|
||||||
Фаза 3 (runner): шаг 7 (исполнение сценариев)
|
|
||||||
Фаза 4 (UI): шаг 8 (интерфейс)
|
|
||||||
Фаза 5: шаг 10 (тесты)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# GPT-5.6 Sol — Идеи развития + поиск багов
|
|
||||||
|
|
||||||
## Что такое autotest
|
|
||||||
|
|
||||||
Flask 3.0 + vanilla JS + PostgreSQL. Развёрнут на Nubes pythonk8s (managed).
|
|
||||||
Ручной инструмент для тестирования Nubes Cloud API — создание/изменение/удаление инстансов любых сервисов (PostgreSQL, Redis, S3, Kafka, ~35 сервисов).
|
|
||||||
|
|
||||||
## Что умеет сейчас (v1.1.44)
|
|
||||||
|
|
||||||
- Ручное создание/изменение/удаление инстансов через UI (как в облаке)
|
|
||||||
- Многопользовательский доступ (свой токен = свой стенд)
|
|
||||||
- История всех операций в PostgreSQL (who/what/when/status)
|
|
||||||
- Полный паритет с Nubes API (все 9 шагов create, валидация, normalize, refSvc)
|
|
||||||
|
|
||||||
## Что хочет заказчик (переписка)
|
|
||||||
|
|
||||||
Файл: `/home/naeel/nubes/autotest/TASKS/3007.md` — прочитай весь.
|
|
||||||
|
|
||||||
Коротко:
|
|
||||||
1. Автоматизированные тесты по конфигурируемым сценариям (не код, а конфиг)
|
|
||||||
2. Последовательности: create → modify → delete для выбранных сервисов
|
|
||||||
3. Общая история операций для всех пользователей
|
|
||||||
4. Учёт карантина (на проде удаление с задержкой 2 недели)
|
|
||||||
5. Тестовая организация (нельзя создавать org на проде)
|
|
||||||
|
|
||||||
## Файлы для анализа
|
|
||||||
|
|
||||||
- `/home/naeel/nubes/autotest/TASKS/3007.md` — требования заказчика
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` — весь фронтенд (460 строк)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` — бэкенд (550 строк)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/app.py` — точка входа
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/db/save_run.py` — сохранение в БД
|
|
||||||
- `/home/naeel/nubes/autotest/HISTORY/2026-07-29-session.md` — что сделано за 2 дня
|
|
||||||
|
|
||||||
## Что нужно
|
|
||||||
|
|
||||||
### 1. Идеи по развитию (главное)
|
|
||||||
|
|
||||||
Исходя из требований заказчика и текущего состояния:
|
|
||||||
- Как должна выглядеть архитектура автотестов? YAML-конфиги → runner → БД?
|
|
||||||
- Как совместить ручной UI и автоматические сценарии?
|
|
||||||
- Как масштабировать на прод (карантин, тестовая организация)?
|
|
||||||
- Что делать с историей — дашборд, алерты при падениях?
|
|
||||||
- Несколько конкретных предложений (от минимального MVP до полной картины)
|
|
||||||
|
|
||||||
### 2. Баги (вторично)
|
|
||||||
|
|
||||||
Просмотри код. Найди что мы пропустили за 44 версии.
|
|
||||||
|
|
||||||
### Требования к ответу
|
|
||||||
|
|
||||||
- Конкретно. Без воды. Без "можно рассмотреть" и "в долгосрочной перспективе".
|
|
||||||
- Каждое предложение — что делать, какой файл менять, какой результат.
|
|
||||||
- Если несколько вариантов — перечислить с плюсами/минусами.
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
# Prompt for Opus — Architecture: Unified Scenario System
|
|
||||||
|
|
||||||
**Ты можешь задавать уточняющие вопросы.** Если чего-то не хватает для принятия решения — спроси. Я (DeepSeek V4 Pro, ассистент Naael) отвечу.
|
|
||||||
|
|
||||||
## ПОРЯДОК ЧТЕНИЯ (обязательно прочитай в этом порядке)
|
|
||||||
|
|
||||||
**Шаг 1 — понять что такое Nubes и как работает API:**
|
|
||||||
`/home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md`
|
|
||||||
|
|
||||||
**Шаг 2 — увидеть дублирование своими глазами (самое важное):**
|
|
||||||
Смотри раздел «КЛЮЧЕВОЙ КОД» ниже — там оба CREATE-флоу (ручной и сценарный) бок о бок.
|
|
||||||
|
|
||||||
**Шаг 3 — понять текущую архитектуру кода:**
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` (строки 175-270 — ручной CREATE)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/operations/scenario.py` (строки 85-210 — сценарный CREATE)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/operations/terraform.py` (общая send_params_terraform)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_scenario_defs.py` (CRUD определений)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/db/scenario_defs.py` (SQL-функции)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/db/init_db.py` (схема БД — найди scenario_definitions)
|
|
||||||
|
|
||||||
**Шаг 4 — понять фронтенд:**
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/templates/index.html` (вся страница, CSS, Jinja2)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/js/operations.js` (showParams, executeOp — как запускается ручная операция)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/js/scenario-form.js` (renderEditor, saveScenario — текущий редактор)
|
|
||||||
|
|
||||||
**НЕ читай — это легаси/устарело:**
|
|
||||||
- `DOCS/ARCHITECTURE.legacy.md`, `DOCS/ARCHITECTURE.md`
|
|
||||||
- `DOCS/sonnet-*.md` — переписки с другим AI
|
|
||||||
- `DOCS/gpt56-*`, `DOCS/questions-to-sol.md`, `DOCS/sol-*`
|
|
||||||
- `HISTORY/` — история сессий
|
|
||||||
- `development/` — HAR-файлы
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Что это за приложение
|
|
||||||
|
|
||||||
**Nubes** — облачная платформа (IaaS/PaaS). ~30 типов сервисов: VM, K8s, PostgreSQL, Redis, S3, Kafka, ClickHouse и др. Каждый сервис имеет операции: create, modify, delete, suspend, resume, redeploy.
|
|
||||||
|
|
||||||
**Autotest** — Flask-приложение, которое делает то же самое что личный кабинет Nubes, но через REST API и автоматически. Два режима:
|
|
||||||
|
|
||||||
1. **Ручной** — пользователь выбирает сервис → видит список autotest-инстансов → кликает → выбирает операцию (modify/delete/suspend/...) → заполняет параметры → запускает. Или жмёт «+ Создать» для CREATE.
|
|
||||||
|
|
||||||
2. **Сценарии** — пользователь создаёт последовательность шагов (create → modify → delete), сохраняет в БД, запускает одним кликом. Каждый шаг = атомарная операция над инстансом.
|
|
||||||
|
|
||||||
### Как работает API Nubes (CREATE)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. POST /instances body={serviceId, displayName, descr} → 201, Location: ./UUID
|
|
||||||
2. POST /instanceOperations body={instanceUid, operation:"create"} → opUid
|
|
||||||
3. GET /instanceOperations/{opUid}?fields=cfsParams → параметры с defaults
|
|
||||||
4. POST /instanceOperationCfsParams (×N — все параметры)
|
|
||||||
5. GET /instanceOperations/{opUid}/validate-cfs → валидация
|
|
||||||
6. POST /instanceOperations/{opUid}/run → запуск
|
|
||||||
7. Поллинг GET /instanceOperations/{opUid}?fields=dtFinish,isSuccessful,... до dtFinish
|
|
||||||
```
|
|
||||||
|
|
||||||
MODIFY/DELETE/SUSPEND/RESUME: шаг 1 пропускается, шаг 2 с `{instanceUid, svcOperationId, operation}` (с svcOperationId!).
|
|
||||||
|
|
||||||
### БД (PostgreSQL, одна на все gunicorn-воркеры)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
scenario_definitions (id, client_id, stand, name, steps JSONB, version, is_active, ...)
|
|
||||||
scenario_runs (id, scenario_name, status, current_step, total_steps, error_log, app_version, ...)
|
|
||||||
runs (id, svc_id, op_name, instance_uid, status, duration_sec, params JSONB, stages JSONB, ...)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## КЛЮЧЕВОЙ КОД — ДУБЛИРОВАНИЕ CREATE-ФЛОУ
|
|
||||||
|
|
||||||
### Ручной режим (api_test.py, функция api_test)
|
|
||||||
|
|
||||||
```python
|
|
||||||
if op_name == "create":
|
|
||||||
display_name = _unique_display_name(client, display_name)
|
|
||||||
descr = f"created by autotest v{current_app.config.get('VERSION', '')}"
|
|
||||||
payload = {"serviceId": svc_id, "displayName": display_name, "descr": descr}
|
|
||||||
resp = client.post("/instances", payload)
|
|
||||||
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
|
|
||||||
if not instance_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить instanceUid"}), 500
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
op_payload = {"instanceUid": instance_uid, "operation": "create"}
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
|
||||||
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
|
|
||||||
if not op_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
send_params_terraform(client, op_uid, params) # ← ОБЩАЯ ФУНКЦИЯ
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, ...), daemon=True).start()
|
|
||||||
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, ...})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Сценарий (scenario.py, функция run_scenario)
|
|
||||||
|
|
||||||
```python
|
|
||||||
if op_name == "create":
|
|
||||||
display_name = f"{AUTOTEST_PREFIX}{scenario_name}-{uuid.uuid4().hex[:6]}"
|
|
||||||
descr = f"scenario {scenario_name} step {step_num}"
|
|
||||||
payload = {"serviceId": svc_id, "displayName": display_name, "descr": descr}
|
|
||||||
resp = client.post("/instances", payload)
|
|
||||||
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
|
|
||||||
if not instance_uid:
|
|
||||||
raise RuntimeError(f"CREATE: no instanceUid in response: ...")
|
|
||||||
instance_map[svc_id] = instance_uid
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
op_payload = {"instanceUid": instance_uid, "operation": op_name} # без svcOperationId
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
|
||||||
op_uid = op_resp.get("instanceOperationUid") or _find_uid(op_resp) or _uid_from_location(...)
|
|
||||||
if not op_uid:
|
|
||||||
raise RuntimeError(f"No opUid in response: ...")
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
from operations.terraform import send_params_terraform
|
|
||||||
send_params_terraform(client, op_uid, resolved_params) # ← ТА ЖЕ ФУНКЦИЯ
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
# ДАЛЕЕ: синхронный while-поллинг (1800s таймаут), save_run(), _save_scenario_run()
|
|
||||||
```
|
|
||||||
|
|
||||||
**Эти два блока делают ОДНО И ТО ЖЕ.** Различаются только:
|
|
||||||
- displayName (autotest-xxx vs autotest-scenario-xxx)
|
|
||||||
- Поллинг (async thread vs sync while)
|
|
||||||
- Сохранение (runs через _finish_op vs runs + scenario_runs)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## КЛЮЧЕВОЙ КОД — ОГРАНИЧЕНИЕ instance_map
|
|
||||||
|
|
||||||
```python
|
|
||||||
# scenario.py, строка 91
|
|
||||||
instance_map = {} # service_id → instanceUid
|
|
||||||
|
|
||||||
# Шаг CREATE:
|
|
||||||
instance_map[svc_id] = instance_uid
|
|
||||||
|
|
||||||
# Шаг НЕ-CREATE:
|
|
||||||
instance_uid = instance_map.get(svc_id)
|
|
||||||
if not instance_uid:
|
|
||||||
raise RuntimeError(f"No instance for service_id {svc_id} — need CREATE first")
|
|
||||||
```
|
|
||||||
|
|
||||||
**Проблема:** привязано к `service_id`. Нельзя:
|
|
||||||
- Два инстанса одного сервиса в сценарии (второй CREATE перезапишет первый)
|
|
||||||
- Сослаться на инстанс из другого сценария
|
|
||||||
- Использовать существующий инстанс по UUID
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Что нужно спроектировать
|
|
||||||
|
|
||||||
### 1. Единый executor (operations/executor.py)
|
|
||||||
|
|
||||||
```python
|
|
||||||
def execute_operation(client, service_id, operation, instance_uid_or_none, params, display_name=None) -> dict:
|
|
||||||
"""
|
|
||||||
Единая точка входа для ручного и сценарного запуска.
|
|
||||||
Возвращает {"instance_uid": ..., "op_uid": ..., "display_name": ...}
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
`api_test.py` и `scenario.py` вызывают эту функцию. Поллинг и save_run — снаружи (у каждого свой).
|
|
||||||
|
|
||||||
### 2. Гибкие ссылки на инстансы
|
|
||||||
|
|
||||||
Новый формат шага в `scenario_definitions.steps`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{"service_id": 1, "operation": "create", "params": {...}, "output": "d1"},
|
|
||||||
{"service_id": 1, "operation": "modify", "params": {...}, "instance_ref": "d1"},
|
|
||||||
{"service_id": 90, "operation": "create", "params": {...}, "output": "pg"},
|
|
||||||
{"service_id": 1, "operation": "delete", "params": {}, "instance_uid": "UUID-явно"}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Резолвинг на бэкенде: `output` → сохраняем в словарь `{name: instance_uid}`. `instance_ref` → берём из словаря. `instance_uid` → используем как есть.
|
|
||||||
|
|
||||||
### 3. UI редактора сценариев
|
|
||||||
|
|
||||||
**Текущее:** inline-форма в `scenario-body`, сервис = numeric input, операция = text input (БАГ), параметры = key:value строки.
|
|
||||||
|
|
||||||
**Нужно:** полноценный редактор с:
|
|
||||||
- Выпадающий список сервисов (`GET /api/services`)
|
|
||||||
- Выпадающий список операций (`GET /api/operations/{svcId}`)
|
|
||||||
- Параметры с автоподгрузкой из `/api/params/{svcOpId}`: name, type, default, valueList, dataDescriptor
|
|
||||||
- Поле `output` для create-шагов (имя для ссылок)
|
|
||||||
- Дропдаун `instance_ref` для не-create шагов (output-имена предыдущих шагов)
|
|
||||||
- `[↑][↓]` для перестановки шагов
|
|
||||||
|
|
||||||
**Вопросы:**
|
|
||||||
1. Модальное окно или раскрытие внутри `scenario-body`? Аргументируй.
|
|
||||||
2. Как показывать параметры: таблица (name|type|default|value) или упрощённо (key=value)?
|
|
||||||
3. pre-fill параметров при смене операции — авто или по кнопке?
|
|
||||||
4. Куда скроллится страница при открытии редактора?
|
|
||||||
|
|
||||||
### 4. Документация для чтения (кроме кода)
|
|
||||||
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md` ← обязательно
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/ARCHITECTURE-FULL.md` ← общая архитектура
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/architecture-final.md` ← финальная версия
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Вопросы
|
|
||||||
|
|
||||||
1. **Unified executor:** сигнатура, возврат, обработка ошибок на каждом шаге CREATE-флоу
|
|
||||||
2. **Формат шагов:** как парсить `output`/`instance_ref`/`instance_uid`, валидация, резолвинг на бэкенде
|
|
||||||
3. **Схема БД:** нужны ли изменения в `scenario_definitions.steps`? Новая колонка для output-блоков?
|
|
||||||
4. **UI редактора:** модал vs inline, компоновка блоков, автоподгрузка параметров, скролл
|
|
||||||
5. **Миграция:** что делать с существующим dummy_test при смене формата шагов
|
|
||||||
6. **Порядок:** в какой последовательности реализовывать
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# 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 — я не смогу прочитать. Только в чат.
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Вопросы к Опусу — 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;`.
|
|
||||||
На кнопке `▶` — не надо.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Пиши ответ прямо в чат текстом.
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
# Opus Implementation Plan — Унификация сценариев autotest (2026-07-31)
|
|
||||||
|
|
||||||
> Основано на DOCS/opus-questions-2026-07-31.md (19 Q+A) и ревью пользователя.
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
Устранить дублирование CREATE-флоу (ручной api_test.py vs сценарный scenario.py),
|
|
||||||
ввести единый execute_operation, гибкие ссылки на инстансы (output/instance_ref/instance_uid),
|
|
||||||
модальный редактор сценариев с богатым рендером параметров.
|
|
||||||
|
|
||||||
## Порядок реализации (E19): бэкенд → формат → миграция вызовов → UI
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Фаза 1 — Общие модули (фундамент)
|
|
||||||
|
|
||||||
### 1. api/utils.py (NEW)
|
|
||||||
- `find_uid(resp)` — поиск UUID по ключам: instanceOperationUid → instanceUid → uid.
|
|
||||||
- `uid_from_location(loc)` — UUID из Location-заголовка.
|
|
||||||
- Заменяет 2 дубля (в api_test.py и scenario.py — сейчас разные реализации).
|
|
||||||
|
|
||||||
### 2. operations/poll.py (NEW)
|
|
||||||
- `poll_until_done(client, op_uid, timeout=1800)` → dict {status, is_successful,
|
|
||||||
error_log, stages, duration, svc}.
|
|
||||||
- Критерий завершения: dtFinish != "" (НЕ isInProgress).
|
|
||||||
- Общий цикл для async (_finish_op) и sync (run_scenario). Убирает хардкод 1800s в 3 местах.
|
|
||||||
|
|
||||||
### 3. operations/executor.py (NEW)
|
|
||||||
- `execute_operation(client, service_id, operation, instance_uid, params,
|
|
||||||
svc_op_id=None, display_name=None)` → dict {ok, error, failed_step,
|
|
||||||
instance_uid, op_uid, display_name}.
|
|
||||||
- Делает всё ДО /run включительно: POST /instances (только create) →
|
|
||||||
POST /instanceOperations → send_params_terraform → POST /run. НЕ поллит.
|
|
||||||
- Ветвится по operation=="create" ВНУТРИ:
|
|
||||||
- create: POST /instances → op_payload {instanceUid, operation}; svc_op_id игнорируется.
|
|
||||||
- non-create: op_payload {instanceUid, svcOperationId, operation}.
|
|
||||||
- failed_step ∈ {instances, instanceOperations, params, run}.
|
|
||||||
- tracker_add вызывается ВНУТРИ executor для всех create (E3) — сразу после
|
|
||||||
получения instanceUid, до params/run (защита от сирот).
|
|
||||||
- Переиспользует существующую send_params_terraform (operations/terraform.py) как есть.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Фаза 2 — Формат шагов и резолвинг (depends Фаза 1)
|
|
||||||
|
|
||||||
### 4. routes/api_scenario_defs.py — _validate_steps
|
|
||||||
Новые опциональные ключи шага: output, instance_ref, instance_uid.
|
|
||||||
Валидация:
|
|
||||||
- output уникален в пределах сценария.
|
|
||||||
- instance_ref ссылается на output из ПРЕДЫДУЩИХ шагов.
|
|
||||||
- для не-create шага обязателен instance_ref | instance_uid | (fallback старый формат).
|
|
||||||
- Старый формат (только service_id) продолжает проходить валидацию.
|
|
||||||
|
|
||||||
### 5. operations/scenario.py — резолвинг инстанса
|
|
||||||
Приоритет: instance_uid > instance_ref > instance_map[service_id] (fallback).
|
|
||||||
После create: bindings[output] = instance_uid — в память И в
|
|
||||||
scenario_runs.instance_bindings (колонка JSONB уже есть, DEFAULT '{}').
|
|
||||||
Резолвинг во время выполнения — ТОЛЬКО из памяти (БД для наблюдаемости).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Фаза 3 — Миграция вызывающих (depends Фаза 1-2)
|
|
||||||
|
|
||||||
### 6. routes/api_test.py
|
|
||||||
- CMDB delete ОСТАЁТСЯ как предпроверка ПЕРЕД executor: при cmdb_ok → early return
|
|
||||||
(opUid="cmdb-...", без /run, без поллинга). Иначе → execute_operation.
|
|
||||||
- Остальные операции: заменить inline флоу на execute_operation.
|
|
||||||
- _finish_op использует poll_until_done (save_run, _op_results, tracker_remove(delete)
|
|
||||||
остаются здесь).
|
|
||||||
- Импорт find_uid/uid_from_location из api/utils.py; удалить локальные копии.
|
|
||||||
|
|
||||||
### 7. operations/scenario.py
|
|
||||||
- Заменить inline флоу на execute_operation.
|
|
||||||
- sync while-поллинг → poll_until_done.
|
|
||||||
- Удалить локальные _find_uid/_uid_from_location.
|
|
||||||
|
|
||||||
### 8. db/init_db.py — startup cleanup (E16)
|
|
||||||
В init_db() добавить:
|
|
||||||
UPDATE scenario_runs SET status='TIMEOUT', error_log='worker restart'
|
|
||||||
WHERE status='RUNNING' AND created_at < NOW() - INTERVAL '1 hour';
|
|
||||||
(init_db вызывается из pool._ensure_schema() раз на воркер, идемпотентно).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Фаза 4 — UI редактора (depends Фаза 2)
|
|
||||||
|
|
||||||
### 9. static/js/params-render.js (NEW)
|
|
||||||
Вынести из operations.js:
|
|
||||||
- renderParamRow(p, allInst) — уже чистая, без глобалов.
|
|
||||||
- renderMapFixedRow(p, dfl) — чистая.
|
|
||||||
- collectParams(containerSelector='#params-form') — параметризовать контейнер
|
|
||||||
(единственная правка сигнатуры; сейчас хардкодит #params-form).
|
|
||||||
Глобалы AUTOTEST_PREFIX/currentSvcId/makeCreateDisplayName остаются в operations.js.
|
|
||||||
_esc (utils.js) и validateJson — общие глобалы.
|
|
||||||
|
|
||||||
### 10. static/js/operations.js
|
|
||||||
Использовать общий params-render.js (удалить дубли рендера).
|
|
||||||
|
|
||||||
### 11. static/js/scenario-form.js — модальный редактор
|
|
||||||
- Модал на весь экран (не inline scenario-body — параметры map-fixed слишком тесны).
|
|
||||||
- Дропдаун сервисов (GET /api/services).
|
|
||||||
- Дропдаун операций (GET /api/operations/{svcId}).
|
|
||||||
- Авто pre-fill параметров при смене операции (GET /api/params/{svcOpId}),
|
|
||||||
показать ВСЕ параметры с defaults + кнопка «Сбросить на defaults».
|
|
||||||
- Поле output для create-шагов.
|
|
||||||
- Дропдаун instance_ref для не-create (output'ы предыдущих шагов).
|
|
||||||
- Кнопки [↑][↓] перестановки шагов.
|
|
||||||
- В БД — символические имена параметров ({"durationMs":"5000"}), резолв в numeric ID в runtime.
|
|
||||||
|
|
||||||
### 12. templates/index.html
|
|
||||||
Разметка модала + подключить params-render.js.
|
|
||||||
|
|
||||||
### 13. app.py — bump VERSION.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Решения (зафиксировано)
|
|
||||||
- Схема БД без изменений (C9), только новые ключи в steps JSONB.
|
|
||||||
- Обе версии формата параллельно, без миграции данных (B8), fallback на service_id.
|
|
||||||
- _op_results в памяти — вне scope (E18).
|
|
||||||
- lock_check (один RUNNING сценарий) сохраняется (E17).
|
|
||||||
- instance_bindings (JSONB) — используется (C10).
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
1. py_compile для .py, node -c для .js.
|
|
||||||
2. Ручной CREATE через UI → инстанс создан, в трекере, запись в runs.
|
|
||||||
3. Ручной DELETE → CMDB-путь работает (early return).
|
|
||||||
4. Сценарий старый формат (seed dummy_test) → работает без изменений.
|
|
||||||
5. Сценарий новый формат: create output:d1 → modify instance_ref:d1 →
|
|
||||||
delete instance_ref:d1 → все OK, instance_bindings заполнен.
|
|
||||||
6. Два create одного сервиса с разными output → два разных инстанса.
|
|
||||||
7. Валидация: instance_ref на несуществующий output → ошибка при сохранении.
|
|
||||||
8. Startup-cleanup: зависший RUNNING >1ч → TIMEOUT после рестарта.
|
|
||||||
|
|
||||||
## Файлы
|
|
||||||
NEW: operations/executor.py, operations/poll.py, api/utils.py, static/js/params-render.js
|
|
||||||
MOD: routes/api_test.py, operations/scenario.py, routes/api_scenario_defs.py,
|
|
||||||
db/init_db.py, static/js/scenario-form.js, static/js/operations.js,
|
|
||||||
templates/index.html, app.py (VERSION)
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# Opus Questions + Answers — 2026-07-31
|
|
||||||
|
|
||||||
## A. Единый executor (operations/executor.py)
|
|
||||||
|
|
||||||
**A1. Граница executor: до /run включительно, без поллинга.**
|
|
||||||
Executor делает все шаги: POST /instances → POST /instanceOperations → send_params_terraform → /run. Возвращает `{instance_uid, op_uid, display_name}`. Поллинг — забота вызывающего (api_test.py: async thread, scenario.py: sync while).
|
|
||||||
|
|
||||||
**A2. Ошибки: dict `{ok, error, failed_step}`.**
|
|
||||||
Не исключения. `failed_step` = "instances" | "instanceOperations" | "params" | "run". Оба вызывающих конвертируют в свой формат (jsonify / RuntimeError).
|
|
||||||
|
|
||||||
**A3. Трекер: да, вызывать tracker_add внутри executor для всех create.**
|
|
||||||
Сценарные инстансы — такие же реальные инстансы в облаке, должны быть в трекере. Единообразие.
|
|
||||||
|
|
||||||
**A4. _finish_op: оставить в api_test.py.**
|
|
||||||
Но вынести цикл поллинга в общий хелпер `poll_until_done(client, op_uid, timeout=1800)` в `operations/poll.py`, который используют и _finish_op (async), и run_scenario (sync).
|
|
||||||
|
|
||||||
## B. Гибкие ссылки на инстансы
|
|
||||||
|
|
||||||
**B5. Приоритет: да. instance_uid > instance_ref > новый create.**
|
|
||||||
|
|
||||||
**B6. Хранение output→uid: и в памяти, и в БД.**
|
|
||||||
В памяти — словарь для быстрого резолвинга во время выполнения. В БД — `scenario_runs.instance_bindings` обновляется после каждого create-шага. При падении воркера видно что создалось. Но runner не зависит от БД для резолвинга (только память).
|
|
||||||
|
|
||||||
**B7. Валидация: да на всё.**
|
|
||||||
`_validate_steps()` проверяет: (a) instance_ref ссылается на output из предыдущих шагов, (b) output уникален в пределах сценария, (c) для не-create шага обязателен один из instance_ref/instance_uid.
|
|
||||||
|
|
||||||
**B8. Обратная совместимость: поддерживать оба формата, без миграции.**
|
|
||||||
Если шаг без output/instance_ref/instance_uid → fallback на старый instance_map[service_id]. Seed dummy_test работает как есть. Новые сценарии используют новый формат.
|
|
||||||
|
|
||||||
## C. Схема БД
|
|
||||||
|
|
||||||
**C9. Да, без изменений схемы.** Только новые опциональные ключи в JSONB-объектах шагов.
|
|
||||||
|
|
||||||
**C10. Да, использовать instance_bindings.** После create: `bindings[output_name] = instance_uid`. Сохранять в БД после каждого шага.
|
|
||||||
|
|
||||||
## D. UI редактора сценариев
|
|
||||||
|
|
||||||
**D11. Модальное окно на весь экран.** Параметры с dataDescriptor (map-fixed) — много вложенных полей, inline в scenario-body слишком тесно.
|
|
||||||
|
|
||||||
**D12. Да, вынести рендер параметров в общий модуль.**
|
|
||||||
`static/js/params-render.js`: `renderParamRow()`, `renderMapFixedRow()`, `collectParams()`. Используется и в operations.js, и в scenario-form.js.
|
|
||||||
|
|
||||||
**D13. Pre-fill: авто при смене операции.** Показывать ВСЕ параметры с defaults, пользователь удаляет лишние или меняет значения. Кнопка «Сбросить на defaults».
|
|
||||||
|
|
||||||
**D14. Символические имена в БД.** `{"durationMs": "5000"}` — человекочитаемо, стабильно. Резолвинг в numeric ID — в runtime.
|
|
||||||
|
|
||||||
## E. Оптимизация
|
|
||||||
|
|
||||||
**E15. Да, вынести в `api/utils.py`.** Обе реализации (_find_uid, _uid_from_location) унифицировать.
|
|
||||||
|
|
||||||
**E16. Да, startup check.** При старте: `UPDATE scenario_runs SET status='TIMEOUT', error_log='worker restart' WHERE status='RUNNING' AND created_at < NOW() - INTERVAL '1 hour'`. Просто, без доп. инфраструктуры.
|
|
||||||
|
|
||||||
**E17. Оставить lock_check.** Один сценарий за раз — безопасно. Можно ослабить позже.
|
|
||||||
|
|
||||||
**E18. Не в scope.** fallback на API работает, потеря stages — minor. Отдельная задача.
|
|
||||||
|
|
||||||
**E19. Порядок: бэкенд → UI.**
|
|
||||||
1. `operations/executor.py` + `operations/poll.py` + `api/utils.py`
|
|
||||||
2. Формат шагов (output/instance_ref) + валидация + резолвинг
|
|
||||||
3. Минимальные правки api_test.py и scenario.py (вызов executor)
|
|
||||||
4. UI редактор (модал + общий рендер параметров)
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# 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. Общая архитектура: все ли зависимости корректны? Нет ли циклических импортов?
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
# План: мок-полигон для интеграционных тестов
|
|
||||||
|
|
||||||
Дата: 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,73 +0,0 @@
|
|||||||
# Вопросы к Sol — обсуждение MVP
|
|
||||||
|
|
||||||
## Файлы для контекста (прочитай перед ответом)
|
|
||||||
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` — текущий 9-шаговый CREATE + non-CREATE
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api.py` — legacy эндпоинты (предлагаешь удалить)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/runner.py` — legacy runner (предлагаешь удалить)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/config.yaml` — предлагаешь удалить
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/db/init_db.py` — схема БД
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/db/save_run.py` — INSERT в runs
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` — фронтенд
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/api/http_client.py` — HTTP-клиент
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/api/auth.py` — токены
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/app.py` — точка входа, blueprint'ы
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/DOCS/gpt56-sol-plan.md` — твой план
|
|
||||||
|
|
||||||
## executor
|
|
||||||
|
|
||||||
**Q1: Зачем выносить executor из `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` прямо сейчас?**
|
|
||||||
Сейчас 9-шаговая логика CREATE и non-CREATE живёт в api_test.py:173-250 (функция `api_test()` + `_send_params_terraform()` + `_normalize_value()` + `_resolve_ref_svc()`). Вынос в отдельный `operation_executor.py` — рефакторинг с риском сломать CREATE. Почему не обернуть вызов существующего `api_test` из runner, а вынос сделать фазой 2?
|
|
||||||
|
|
||||||
**Q2: executor внутри того же gunicorn-воркера или отдельный процесс?**
|
|
||||||
Сценарий PostgreSQL — 10-20 минут. Redis — 3 минуты. Блокирует ли executor gunicorn-воркера на всё это время? Или запускать в `threading.Thread` как сейчас делает `api_test`?
|
|
||||||
|
|
||||||
## YAML
|
|
||||||
|
|
||||||
**Q3: Параметры — символьный code или numeric ID?**
|
|
||||||
Предлагаешь code: `{code: cpu, value: "500"}`. Это читаемо, но требует резолва через `/instanceOperations/default/{opId}` → найти `svcOperationCfsParamId` для кода `cpu`. Числовой ID `{104: "500"}` работает сразу, но нечитаемо. Что предлагаешь для MVP?
|
|
||||||
|
|
||||||
**Q4: lifecycle persistent — как найти «тот самый» instance?**
|
|
||||||
По `displayName == "autotest-postgres-permanent"` через `GET /instances`? А если кто-то переименовал в UI облака? А если два сценария случайно создали инстансы с одинаковым именем?
|
|
||||||
|
|
||||||
**Q5: cleanup `always` — что чистить если create упал?**
|
|
||||||
Если `POST /instances` вернул ошибку — `instanceUid` пустой. Вызывать `POST /instanceOperations {operation: "delete"}` не на чем. Что удалять?
|
|
||||||
|
|
||||||
## CMDB DELETE
|
|
||||||
|
|
||||||
**Q6: Почему удалить CMDB DELETE из `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py`?**
|
|
||||||
Сейчас строка 217-222: CMDB DELETE для not-created инстансов. Nubes API (`POST /instanceOperations {operation: "delete"}` + `/run`) возвращает 422 «Отсутствует state» для not-created. Без CMDB у пользователя нет способа удалить битые инстансы кроме ручного UI облака. Может оставить но с логом, а не удалять?
|
|
||||||
|
|
||||||
## БД
|
|
||||||
|
|
||||||
**Q7: Зачем три таблицы (`scenario_runs` + `scenario_steps` + новые колонки в `runs`)?**
|
|
||||||
Почему не две (`scenario_runs` + `scenario_steps`) и не менять существующую `runs`? Каждый шаг сценария уже вызывает `save_run()` из `/home/naeel/nubes/autotest/app-autotest/site/db/save_run.py`, который пишет в `runs`. Можно добавить `scenario_run_id` в `runs` без новой таблицы `scenario_steps`.
|
|
||||||
|
|
||||||
**Q8: heartbeat — через БД? UPDATE каждые N секунд?**
|
|
||||||
Не убьёт ли это PostgreSQL при частых сценариях? Альтернатива — файловый lock как в `/home/naeel/nubes/autotest/app-autotest/site/operations/tracker.py` (fcntl.flock).
|
|
||||||
|
|
||||||
## UI
|
|
||||||
|
|
||||||
**Q9: Вкладки — зачем?**
|
|
||||||
Сейчас всё на одном экране (`/home/naeel/nubes/autotest/app-autotest/site/templates/index.html`). Добавление вкладок раздувает UI. Почему не добавить секцию «Сценарии» под списком инстансов, без переделки всего экрана?
|
|
||||||
|
|
||||||
**Q10: Запрет innerHTML для error/log — почему?**
|
|
||||||
Сейчас весь вывод идёт через `_esc()` (строка 263 app.js). Что конкретно не экранируется?
|
|
||||||
|
|
||||||
## Prod policy
|
|
||||||
|
|
||||||
**Q11: Почему такая сложная политика для MVP?**
|
|
||||||
Проверка org UID, ClientID, allowlist delete, quarantine, purge_not_before — это не MVP. Почему не просто `if stand == "prod": return {"error": "prod заблокирован для сценариев"}` на первом этапе? Всё остальное можно добавить когда сценарии заработают на dev/test.
|
|
||||||
|
|
||||||
## Legacy
|
|
||||||
|
|
||||||
**Q12: `/home/naeel/nubes/autotest/app-autotest/site/config.yaml` — что в нём ценного?**
|
|
||||||
Ты предлагаешь удалить. Но там могут быть настройки которые пригодятся. Что именно предлагаешь перенести в `app.py`?
|
|
||||||
|
|
||||||
## Общие вопросы
|
|
||||||
|
|
||||||
**Q13: Порядок шагов — почему удаление legacy (шаг 1) до создания executor (шаг 2)?**
|
|
||||||
Если executor будет вызывать код из `api_test.py`, удаление `api.py` и `runner.py` безопасно делать первым — они не используются. Но `config.yaml` используется в `runner.py:load_config()`. Если удалить config.yaml до того как executor готов — не сломает ли это что-то?
|
|
||||||
|
|
||||||
**Q14: Риски совместимости — ручной UI не сломается?**
|
|
||||||
После всех изменений ручные операции в UI должны работать как раньше. Где самое узкое место? `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py:api_test()` — основной обработчик, если его менять — риск для всего.
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Сравнение: Sonnet 4.6 vs Opus 4.8 — полный код-ревью v1.1.20
|
|
||||||
|
|
||||||
## Совпадения (оба нашли)
|
|
||||||
|
|
||||||
| Баг | Sonnet | Opus |
|
|
||||||
|-----|--------|------|
|
|
||||||
| `_finish_op` args CREATE | 🔴 | 🔴 |
|
|
||||||
| `api_history()` connection leak | 🔴 | 🟠 |
|
|
||||||
| XSS displayName/svc | 🟡 | 🟡 |
|
|
||||||
| Мёртвый код/docstring'и | 🟠 | 🟢 |
|
|
||||||
|
|
||||||
## Только Opus (дополнительно)
|
|
||||||
|
|
||||||
| Баг | |
|
|
||||||
|-----|---|
|
|
||||||
| `detect_endpoint()` — 2-3 лишних HTTP на каждый запрос | 🟠 |
|
|
||||||
| `_op_results` in-memory dict — не переживает multi-worker | 🟠 |
|
|
||||||
| CSRF на POST (частично закрыто SameSite) | 🟡 |
|
|
||||||
| `currentSvcShort` без `let` — неявная глобальная | 🟢 |
|
|
||||||
| Дублирование рендера в selectService/refreshInstances | 🟢 |
|
|
||||||
| CREATE шаг 6: шаблон vs реальная операция | 🟢 |
|
|
||||||
|
|
||||||
## Только Sonnet
|
|
||||||
|
|
||||||
| Баг | |
|
|
||||||
|-----|---|
|
|
||||||
| pool.py `_initialized=True` до `init_db()` | 🔴 |
|
|
||||||
| Cookie без `secure=True` | 🟡 |
|
|
||||||
| MODIFY без required+default | 🟡 |
|
|
||||||
| pool.py race condition без мьютекса | 🟠 |
|
|
||||||
|
|
||||||
## Мой анализ
|
|
||||||
|
|
||||||
Opus 4.8 глубже — проверил эталонный Terraform flow, подтвердил что CREATE-флоу соответствует, нашёл проблемы с `detect_endpoint` и multi-worker. Sonnet нашёл pool.py баги, которые Opus пропустил.
|
|
||||||
|
|
||||||
**План исправлений (по приоритету):**
|
|
||||||
|
|
||||||
1. 🔴 `_finish_op` args — добавить `False` для `is_delete` в CREATE-ветку
|
|
||||||
2. 🔴 `api_history()` — `try/finally: put_conn(conn)`
|
|
||||||
3. 🔴 `pool.py` — `_initialized=True` после успешного `init_db()`
|
|
||||||
4. 🟡 XSS — `_esc()` для displayName/svc в рендере инстансов + истории + stages
|
|
||||||
5. 🟡 Cookie `secure=True`
|
|
||||||
6. 🟠 `currentSvcShort` → `let currentSvcShort`
|
|
||||||
|
|
||||||
Правки 1-3 критические — их делаю первыми. 4-6 следом.
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Sol — ответы на 14 вопросов
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
|
|
||||||
## Ключевые решения
|
|
||||||
|
|
||||||
| # | Решение |
|
|
||||||
|---|---------|
|
|
||||||
| executor | Извлечь синхронное ядро, сохранить POST /api/test |
|
|
||||||
| threading | Один threading.Thread, PG как источник статуса |
|
|
||||||
| YAML params | Символьные коды, резолв через API при запуске |
|
|
||||||
| persistent | Таблица привязок scenario+stand+key+svc→UID |
|
|
||||||
| cleanup | SKIP если нет UID, try delete если частичный |
|
|
||||||
| CMDB | Убрать из авто-delete. Force-cleanup эндпоинт для dev/test |
|
|
||||||
| БД | scenario_runs + расширить runs (не три таблицы) |
|
|
||||||
| heartbeat | UPDATE в PG раз в 30с |
|
|
||||||
| UI | Сворачиваемая секция, без вкладок |
|
|
||||||
| escape | statusError, d.error, log, currentSvcName |
|
|
||||||
| prod | Заблокировать целиком для MVP |
|
|
||||||
| config.yaml | dummy→сценарий, PG→пример |
|
|
||||||
| порядок | mock → executor → route → YAML → удалить legacy |
|
|
||||||
| риск | POST /api/test формат не менять |
|
|
||||||
|
|
||||||
## MVP: dev/test only, symbolic codes, один поток, PG статус, нет вкладок, нет CMDB-auto.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# Sol — ответы по редактору сценариев
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
|
|
||||||
## 1. Структура `steps`
|
|
||||||
- JSONB массив, отдельная таблица не нужна
|
|
||||||
- Обязательно поле `resource` — идентификатор инстанса внутри сценария (для связи create→modify→delete)
|
|
||||||
- `scenario_definitions`: + `version INTEGER`, `updated_by`, `is_active`/`deleted_at`, уникальность имени case-insensitive
|
|
||||||
- `scenario_runs`: + `definition_id`, `definition_version`, `steps_snapshot`
|
|
||||||
- Runner работает со snapshot, не перечитывает определение
|
|
||||||
|
|
||||||
## 2. Параметры в редакторе
|
|
||||||
- Из Nubes API: сервис→операции→`/instanceOperations/default/{opId}`→коды
|
|
||||||
- Выпадающий список с типом, обязательностью, default
|
|
||||||
- Map/array — JSON-строка с валидацией
|
|
||||||
- Кеш: браузерный на время сессии редактора ИЛИ серверный (stand+opId, TTL 5 мин)
|
|
||||||
- Свободный ввод кода не нужен
|
|
||||||
|
|
||||||
## 3. Seed из config.yaml
|
|
||||||
- Не импортировать при каждом пустом старте
|
|
||||||
- Схема: marker импорта → `INSERT ON CONFLICT DO NOTHING` → marker `scenario_seed_v1=completed`
|
|
||||||
- YAML вынести в `scenario_seed.yaml`, после rollout удалить
|
|
||||||
- Остальной config.yaml не трогать
|
|
||||||
|
|
||||||
## 4. UI редактор
|
|
||||||
- Модальное окно или боковая панель (не inline)
|
|
||||||
- Имя, список шагов, сервис, операция, параметры
|
|
||||||
- Кнопки добавить/удалить/вверх/вниз
|
|
||||||
- Drag-and-drop не нужен
|
|
||||||
- Мягкое удаление: `is_active=false`
|
|
||||||
|
|
||||||
## 5. Валидация
|
|
||||||
- При сохранении: структура, существование сервиса, доступность операции, коды параметров, типы, уникальность resource, порядок (create→modify→delete, после delete ничего)
|
|
||||||
- При запуске: повторить по актуальному API
|
|
||||||
- Невалидный сценарий не запускать частично
|
|
||||||
|
|
||||||
## 6. Критически упущенное
|
|
||||||
|
|
||||||
1. `POST /api/scenario/run` не возвращает run ID — гонка при двух запусках. Создавать запись ДО thread, вернуть HTTP 202 + run_id
|
|
||||||
2. Нужен `GET /api/scenario/runs/<id>` — поллинг конкретного запуска
|
|
||||||
3. `save_run()` не пишет `scenario_run_id` и `step_number` — шаги не связаны со сценарием
|
|
||||||
4. TIMEOUT: `op_data` может быть неинициализирована
|
|
||||||
5. Шаг нужно создавать в `runs` со статусом RUNNING ДО обращения к API, потом обновлять
|
|
||||||
6. Блокировка параллельных запусков: HTTP 409 для одного client_id+stand
|
|
||||||
7. CRUD: проверять `client_id+stand` при всех операциях
|
|
||||||
8. `version` в definitions: PUT с текущей version, 409 при конфликте
|
|
||||||
9. Runner использует персональный токен — нужен сервисный токен/клиент по stand
|
|
||||||
10. Имя не идентификатор: API запуска принимает `definition_id`, имя — редактируемое поле
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
# Запрос к Claude (Sonnet): архитектурный аудит — Round 2
|
|
||||||
|
|
||||||
> **Контекст**: Round 1 анализ получен, исправления по приоритетам приняты.
|
|
||||||
> **Новое**: проект будет подключать **PostgreSQL** (отдельный сервис Nubes).
|
|
||||||
> **Дата**: 27.07.2026
|
|
||||||
> **Версия**: 1.0.89
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Контекст Round 1
|
|
||||||
|
|
||||||
Sonnet в первом раунде выявил:
|
|
||||||
- 🔴 Утечка `_op_results` — нужно TTL/очистка
|
|
||||||
- 🔴 Cookie без `httponly`/`samesite`
|
|
||||||
- 🟡 Дублирование `_client_id()` в main.py и api_test.py
|
|
||||||
- 🟡 Захардкожен `SVC_ID=1` — хотя `/api/services` существует
|
|
||||||
- 🟢 Рекомендация: SQLite для истории → **теперь PostgreSQL**
|
|
||||||
|
|
||||||
Все исправления из Round 1 приняты к реализации. Начинаем со среднего приоритета (общий модуль `auth.py`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Новый контекст: PostgreSQL
|
|
||||||
|
|
||||||
Проект получает отдельный PostgreSQL-сервис в Nubes. Нужно спроектировать:
|
|
||||||
1. **Схему БД** для хранения истории тестов
|
|
||||||
2. **Подключение** (connection pool для multi-worker gunicorn)
|
|
||||||
3. **Миграции** (как управлять схемой при обновлениях)
|
|
||||||
4. **API** для чтения/записи результатов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Вопросы Round 2
|
|
||||||
|
|
||||||
### 1. PostgreSQL — схема данных
|
|
||||||
|
|
||||||
Нужна схема для истории запусков. Минимальный набор полей:
|
|
||||||
- id, created_at
|
|
||||||
- client_id, stand (изоляция по пользователю и стенду)
|
|
||||||
- svc_id, svc_name
|
|
||||||
- op_name (create/modify/delete/...), svc_op_id
|
|
||||||
- instance_uid, display_name
|
|
||||||
- status (OK/FAIL/TIMEOUT), duration_sec
|
|
||||||
- error_log (текст ошибки)
|
|
||||||
- params (JSON — все параметры запуска)
|
|
||||||
- stages (JSON — этапы выполнения с длительностью)
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Какие индексы нужны? (по client_id+stand, по created_at, по instance_uid?)
|
|
||||||
- Нужна ли отдельная таблица для stages или JSON в основном поле?
|
|
||||||
- Нужна ли таблица `instances` (связь instance_uid → последний статус)?
|
|
||||||
- Как хранить чувствительные params (пароли, токены в map-параметрах)?
|
|
||||||
|
|
||||||
### 2. PostgreSQL — connection pool
|
|
||||||
|
|
||||||
Сейчас gunicorn с несколькими воркерами. Каждый воркер — отдельный процесс.
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Какой connection pooler использовать? (psycopg2.pool? SQLAlchemy? asyncpg?)
|
|
||||||
- Где создавать пул? (в app.py при старте? lazy в каждом воркере?)
|
|
||||||
- Как передавать соединение в роуты? (flask.g? внедрение через blueprint?)
|
|
||||||
- Нужен ли `@app.teardown_appcontext` для возврата соединения в пул?
|
|
||||||
|
|
||||||
### 3. PostgreSQL — миграции
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Нужна ли полноценная миграция (Alembic) или простая схема в `init_db.py`?
|
|
||||||
- Где хранить SQL-файлы миграций? (отдельная папка `migrations/`?)
|
|
||||||
- Как применять при деплое? (автоматически при старте приложения?)
|
|
||||||
|
|
||||||
### 4. Объединение с текущим трекером
|
|
||||||
|
|
||||||
Сейчас трекер (`tracker.py`) хранит UID'ы в JSON-файлах `/tmp/`. После появления PostgreSQL:
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Заменяет ли PostgreSQL трекер полностью? (хранить UID'ы в БД вместо /tmp/)
|
|
||||||
- Или оставить трекер для краткосрочного кеша, а БД — для истории?
|
|
||||||
- Если заменить — нужна ли миграция существующих данных из /tmp/ в БД?
|
|
||||||
|
|
||||||
### 5. Унификация `auth.py`
|
|
||||||
|
|
||||||
Из Round 1: вынести `_client_id()`, `get_client()`, `get_stand()`, `get_token_info()` в общий модуль `api/auth.py`.
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Использовать `flask.g` + `@bp.before_request` или явные вызовы функций?
|
|
||||||
- Как обрабатывать роуты без обязательного токена (GET / без авторизации)?
|
|
||||||
- Где создавать `HttpClient` — в `before_request` (на каждый запрос) или lazy?
|
|
||||||
|
|
||||||
### 6. Фронтенд — динамические сервисы
|
|
||||||
|
|
||||||
Сейчас `SVC_ID=1` захардкожен. Нужно загружать список из `/api/services`.
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Показывать ВСЕ сервисы или только из whitelist в config.yaml?
|
|
||||||
- Какой UI для переключения между сервисами? (выпадающий список? вкладки?)
|
|
||||||
- При смене сервиса — сбрасывать `selectedInst` и форму параметров?
|
|
||||||
|
|
||||||
### 7. JS в отдельный файл
|
|
||||||
|
|
||||||
Вынести весь JS из `<script>` в `/static/app.js`.
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Нужно ли использовать ES modules (`type="module"`) или классический скрипт?
|
|
||||||
- Если модули — как разбить на файлы (instances.js, params.js, log.js)?
|
|
||||||
- Как передавать Jinja2-переменные (config.VERSION, token_info) в JS-файл?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Файлы для изучения (если нужно)
|
|
||||||
|
|
||||||
Те же что в Round 1, плюс:
|
|
||||||
- `DOCS/HISTORY.md` — обновлён результатами Round 1
|
|
||||||
- `DOCS/sonnet-architecture-review-v1.0.89.md` — запрос Round 1
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
# Запрос к Claude (Sonnet): архитектурный аудит app-autotest v1.0.89
|
|
||||||
|
|
||||||
> **Цель**: получить анализ текущей архитектуры и рекомендации по развитию проекта.
|
|
||||||
> **Дата**: 27.07.2026
|
|
||||||
> **Версия**: 1.0.89
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. О проекте
|
|
||||||
|
|
||||||
### Что делает
|
|
||||||
Flask-приложение для ручного тестирования операций облачной платформы Nubes:
|
|
||||||
- CREATE — создать инстанс сервиса с параметрами
|
|
||||||
- MODIFY — изменить параметры существующего инстанса
|
|
||||||
- DELETE / SUSPEND / RESUME / REDEPLOY — управление инстансом
|
|
||||||
|
|
||||||
### Как работает сейчас
|
|
||||||
- Один HTML-файл (index.html) с ванильным JS — без фреймворков, без SPA
|
|
||||||
- Бэкенд: Flask + gunicorn (несколько воркеров)
|
|
||||||
- Все данные — из REST API Nubes (никакого локального кеша, кроме краткосрочного трекера)
|
|
||||||
- Трекер инстансов: JSON-файлы в /tmp/ под fcntl.flock
|
|
||||||
- Автоопределение стенда (dev/test) по JWT-токену
|
|
||||||
|
|
||||||
### Текущая файловая структура
|
|
||||||
```
|
|
||||||
app-autotest/
|
|
||||||
├── site/
|
|
||||||
│ ├── app.py ← Flask(__name__), blueprints, /health
|
|
||||||
│ ├── api/
|
|
||||||
│ │ └── http_client.py ← HttpClient, автостенд
|
|
||||||
│ ├── operations/
|
|
||||||
│ │ ├── get_params.py ← слияние state.params + шаблон
|
|
||||||
│ │ ├── get_services.py ← API-обёртки
|
|
||||||
│ │ ├── get_instances.py ← пагинация
|
|
||||||
│ │ └── tracker.py ← /tmp/instances-{clientId}-{stand}.json
|
|
||||||
│ ├── routes/
|
|
||||||
│ │ ├── main.py ← GET/POST /, /api/operations/<svc_id>
|
|
||||||
│ │ ├── api.py ← [LEGACY] раннер тестов
|
|
||||||
│ │ └── api_test.py ← /api/test, /api/params, /api/log
|
|
||||||
│ ├── templates/
|
|
||||||
│ │ └── index.html ← весь UI (~400 строк)
|
|
||||||
│ └── static/
|
|
||||||
│ └── style.css
|
|
||||||
├── DOCS/
|
|
||||||
│ ├── ARCHITECTURE.md ← актуальная архитектура
|
|
||||||
│ ├── ARCHITECTURE.legacy.md ← старая версия
|
|
||||||
│ ├── HISTORY.md ← история версий
|
|
||||||
│ └── ... ← 12+ legacy-документов
|
|
||||||
└── secrets/ ← токены (не в git)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ключевые детали реализации
|
|
||||||
- **Multi-worker safety**: все общие данные в /tmp/ под fcntl.flock
|
|
||||||
- **CREATE vs non-CREATE**: раздельные потоки в бэкенде и фронтенде
|
|
||||||
- **Текущие значения параметров**: GET /instances/{uid} → state.params + GET /instanceOperations/default/{opId} → слияние
|
|
||||||
- **Логирование**: /tmp/app-autotest.log + /api/log + скрытая UI-панель
|
|
||||||
- **Валидация**: HTML-escape + JSON-валидация map-полей (onblur + pre-submit)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Что нужно проанализировать
|
|
||||||
|
|
||||||
### A. Decoupling и модульность
|
|
||||||
Сейчас всё в одном HTML и одном api_test.py (300+ строк). Нужны рекомендации:
|
|
||||||
- Как разбить index.html на компоненты/модули?
|
|
||||||
- Как разбить api_test.py на независимые модули по операциям?
|
|
||||||
- Стоит ли переходить на микро-фронтенд (HTMX, Alpine.js, что-то ещё) или оставить ванильный JS?
|
|
||||||
- Как организовать роутинг если добавится больше страниц?
|
|
||||||
|
|
||||||
### B. Хранение данных тестов
|
|
||||||
Сейчас никакие результаты тестов не сохраняются. Нужно:
|
|
||||||
- Где и как хранить историю запусков (каждый запуск = запись)?
|
|
||||||
- Какие данные сохранять: параметры, длительность, статус, этапы, ошибки?
|
|
||||||
- Формат хранения: SQLite? JSON-файлы? Внешняя БД?
|
|
||||||
- Как привязать к конкретному инстансу и пользователю?
|
|
||||||
- Нужна ли страница истории/отчётов?
|
|
||||||
|
|
||||||
### C. Оптимизация для развития
|
|
||||||
- Что мешает добавить второй сервис (сейчас только Болванка, svcId=1)?
|
|
||||||
- Как сделать конфигурацию сервисов (какие сервисы показывать, какие операции)?
|
|
||||||
- Нужен ли config.yaml или всё из API?
|
|
||||||
- Как масштабировать на множество пользователей (изоляция данных)?
|
|
||||||
|
|
||||||
### D. Улучшения архитектуры
|
|
||||||
- Сейчас _client() определяется в двух местах (main.py и api_test.py) — как унифицировать?
|
|
||||||
- Стоит ли вынести общие хелперы (_client_id, _stand, _token_info) в отдельный модуль?
|
|
||||||
- Нужен ли middleware для токена (вместо ручного извлечения в каждом роуте)?
|
|
||||||
- Как улучшить обработку ошибок (сейчас много try/except с голым str(e))?
|
|
||||||
|
|
||||||
### E. Безопасность
|
|
||||||
- Токен передаётся в cookie и форме — достаточно ли этого?
|
|
||||||
- Нужна ли CSRF-защита для POST-запросов?
|
|
||||||
- Логи могут содержать чувствительные данные?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Ожидаемый формат ответа
|
|
||||||
|
|
||||||
### 1. Общая оценка
|
|
||||||
- Что сделано хорошо
|
|
||||||
- Что требует внимания
|
|
||||||
- Критические проблемы (если есть)
|
|
||||||
|
|
||||||
### 2. Decoupling — план
|
|
||||||
- Пошаговый план разбиения на модули
|
|
||||||
- Рекомендации по фронтенду (ванильный JS / HTMX / Alpine / SPA)
|
|
||||||
- Рекомендации по бэкенду (структура модулей)
|
|
||||||
|
|
||||||
### 3. Хранение данных тестов — план
|
|
||||||
- Схема данных (какие поля, какие таблицы/файлы)
|
|
||||||
- Технология (SQLite / PostgreSQL / JSON-файлы)
|
|
||||||
- API для чтения/записи
|
|
||||||
- UI для просмотра истории
|
|
||||||
|
|
||||||
### 4. Оптимизация — конкретные шаги
|
|
||||||
- Приоритизированный список улучшений
|
|
||||||
- Что можно сделать быстро, что требует переписывания
|
|
||||||
|
|
||||||
### 5. Риски и ограничения
|
|
||||||
- Что может пойти не так при предлагаемых изменениях
|
|
||||||
- Ограничения платформы (gunicorn, /tmp/, DDoS-Guard)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Файлы для изучения
|
|
||||||
|
|
||||||
**Код (в порядке важности):**
|
|
||||||
1. `site/routes/api_test.py` — основной модуль операций
|
|
||||||
2. `site/templates/index.html` — весь фронтенд
|
|
||||||
3. `site/routes/main.py` — главная страница и API операций
|
|
||||||
4. `site/api/http_client.py` — HTTP-клиент и автостенд
|
|
||||||
5. `site/operations/get_params.py` — слияние параметров
|
|
||||||
6. `site/operations/tracker.py` — файловый трекер
|
|
||||||
7. `site/app.py` — точка входа
|
|
||||||
8. `site/operations/get_services.py` + `get_instances.py` — API-обёртки
|
|
||||||
|
|
||||||
**Документация:**
|
|
||||||
1. `DOCS/ARCHITECTURE.md` — актуальная архитектура
|
|
||||||
2. `DOCS/HISTORY.md` — история версий
|
|
||||||
3. `DOCS/terraform-operations-full-logic.md` — логика API из Terraform-провайдера
|
|
||||||
|
|
||||||
**Данные (HAR-файлы):**
|
|
||||||
1. `development/dummycreate.har` — полный flow CREATE
|
|
||||||
2. `development/dummymodify.har` — полный flow MODIFY
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Sonnet 4.6 — Финальный аудит: баги и действия пользователя
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
Версия: v1.1.43
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
За 2 дня: 43 версии. Множество правок. Только что нашли баг — в `toggleInstance` удалили `const opsEl = ...` при рефакторинге, упало с ReferenceError. Нужно найти ВСЕ подобные баги.
|
|
||||||
|
|
||||||
## Файлы для проверки (ВСЕ)
|
|
||||||
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` — 460 строк, весь фронтенд
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` — бэкенд
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/templates/index.html` — HTML/CSS
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/api/http_client.py` — HTTP-клиент
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py` — параметры
|
|
||||||
|
|
||||||
## Что искать
|
|
||||||
|
|
||||||
### 1. ReferenceError / undefined variables
|
|
||||||
- Все ли переменные объявлены перед использованием?
|
|
||||||
- `opsEl`, `svcEl`, `btnSpan`, `inst`, `afterEl` — все ли определены в своей области видимости?
|
|
||||||
|
|
||||||
### 2. Все действия пользователя при busy=true
|
|
||||||
Проверь КАЖДУЮ функцию — не вызывает ли она `stopPoll()` до проверки `busy`? Не меняет ли DOM (stages-box, params-area) до проверки?
|
|
||||||
|
|
||||||
Функции для проверки:
|
|
||||||
- `toggleInstance(iuid)` — строка 76
|
|
||||||
- `selectService(svcId)` — строка 39
|
|
||||||
- `startCreate()` — строка 106
|
|
||||||
- `runOp(opName,opId)` — строка 128
|
|
||||||
- `showParams(opId,opName)` — строка 146 (вызывается из runOp, startCreate)
|
|
||||||
- `executeOp(params)` — строка 307
|
|
||||||
- `setFinishedState(...)` — строка 286
|
|
||||||
- `refreshInstances()` — строка 389
|
|
||||||
- `toggleHistory()` — строка 410
|
|
||||||
- `toggleLog()` — строка 456
|
|
||||||
|
|
||||||
### 3. Пустые catch / потерянные ошибки
|
|
||||||
- `showParams` — есть `.catch()` ✅
|
|
||||||
- `executeOp` — есть `catch(e)` + busy ✅
|
|
||||||
- `loadHistory` — есть `catch(e)` ✅
|
|
||||||
- `startCreate` — есть `catch(e)` с fallback ✅
|
|
||||||
- Поллинг в executeOp — `catch(e)` со счётчиком ✅
|
|
||||||
- Есть ли другие `try/catch` или `fetch().then()` без обработки ошибок?
|
|
||||||
|
|
||||||
### 4. busy не сбрасывается
|
|
||||||
Все ли пути выхода из `executeOp` сбрасывают `busy=false`?
|
|
||||||
- FAIL: `busy=false` + return ✅
|
|
||||||
- CMDB OK: `busy=false` + setFinishedState ✅
|
|
||||||
- Поллинг завершился: `setFinishedState` → `busy=false` ✅
|
|
||||||
- catch: `busy=false` ✅
|
|
||||||
- Сетевая ошибка в поллинге: счётчик → `setFinishedState` ✅
|
|
||||||
|
|
||||||
### 5. DOM-элементы
|
|
||||||
- Все ли `getElementById` ссылаются на существующие ID в index.html?
|
|
||||||
- `ops-${iuid}` — генерируется динамически в `selectService` и `refreshInstances`
|
|
||||||
- `param-displayname` — генерируется в `showParams`
|
|
||||||
- Все ли статические ID присутствуют?
|
|
||||||
|
|
||||||
### 6. Параметры операций
|
|
||||||
- `runOp` → `showParams` — правильно ли для ВСЕХ типов операций?
|
|
||||||
- `showParams` → `opHeader` для не-create — показывается всегда ✅
|
|
||||||
- `showParams` → `createHeader` для create — показывается ✅
|
|
||||||
- Пустые params → `opHeader` + кнопка «Запустить» ✅
|
|
||||||
|
|
||||||
### 7. Любые другие баги
|
|
||||||
Что ещё могло сломаться за 43 версии?
|
|
||||||
@@ -1,509 +0,0 @@
|
|||||||
# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
|
|
||||||
|
|
||||||
# Полный обзор кода app-autotest — запрос к Sonnet
|
|
||||||
|
|
||||||
Отправлено: 27.07.2026
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
Flask-приложение на Nubes pythonk8s (gunicorn, 1 worker, нет persistent volume).
|
|
||||||
Репозиторий: https://gitea.services.ngcloud.ru/forcloud/app-autotest.git
|
|
||||||
Деплой: git push → managed service редеплоит.
|
|
||||||
Текущая версия: v1.0.48.
|
|
||||||
|
|
||||||
## ПРОБЛЕМА
|
|
||||||
|
|
||||||
CREATE инстанса через UI: инстанс создаётся в Nubes (виден в UI платформы), НО в нашем списке инстансов НЕ появляется. Происходит СТАБИЛЬНО, 3 раза подряд (v1.0.46, v1.0.47, v1.0.48). Пробовали:
|
|
||||||
- v1.0.46: tracker_add в фоновом daemon-потоке _finish_op
|
|
||||||
- v1.0.47: tracker_add в _finish_op, но до установки _op_results[OK] (попытка победить гонку)
|
|
||||||
- v1.0.48: tracker_add синхронно в api_test() до запуска потока, с try/except: pass
|
|
||||||
|
|
||||||
НИ ОДИН вариант не сработал. Инстанс в трекере не появляется.
|
|
||||||
|
|
||||||
Текущий код на проде показывает 4 инстанса из _INITIAL:
|
|
||||||
```
|
|
||||||
curl-final-test (suspended)
|
|
||||||
curl-test-144834 (suspended)
|
|
||||||
autotest-1 (suspended)
|
|
||||||
autotest-1-first (running)
|
|
||||||
```
|
|
||||||
|
|
||||||
Новый инстанс (f192b10d-beb0-4a25-b79f-5dbd7de4712e, создан в 09:16) — отсутствует.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ВЕСЬ КОД
|
|
||||||
|
|
||||||
### 1. site/app.py — точка входа
|
|
||||||
```python
|
|
||||||
import os
|
|
||||||
from flask import Flask
|
|
||||||
from routes.main import bp as main_bp
|
|
||||||
from routes.api import bp as api_bp
|
|
||||||
from routes.api_test import bp as api_test_bp
|
|
||||||
|
|
||||||
VERSION = "1.0.48"
|
|
||||||
|
|
||||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
|
||||||
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc")
|
|
||||||
app.config["NUBES_API_TOKEN"] = os.getenv("NUBES_API_TOKEN", "")
|
|
||||||
app.config["VERSION"] = VERSION
|
|
||||||
app.register_blueprint(main_bp)
|
|
||||||
app.register_blueprint(api_bp)
|
|
||||||
app.register_blueprint(api_test_bp)
|
|
||||||
|
|
||||||
@app.route("/health")
|
|
||||||
def health():
|
|
||||||
return "OK"
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(host="0.0.0.0", port=5000)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. site/api/http_client.py — HTTP клиент
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
class HttpClient:
|
|
||||||
def __init__(self, endpoint, token):
|
|
||||||
self._endpoint = endpoint.rstrip("/")
|
|
||||||
self._session = requests.Session()
|
|
||||||
self._session.headers.update({
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"User-Agent": "Mozilla/5.0",
|
|
||||||
})
|
|
||||||
|
|
||||||
def get(self, path, **kwargs):
|
|
||||||
kwargs.setdefault("timeout", 10)
|
|
||||||
r = self._session.get(f"{self._endpoint}{path}", **kwargs)
|
|
||||||
r.raise_for_status()
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
def post(self, path, data=None, **kwargs):
|
|
||||||
kwargs.setdefault("timeout", 30)
|
|
||||||
url = f"{self._endpoint}{path}"
|
|
||||||
r = self._session.post(url, json=(data if data is not None else {}), **kwargs)
|
|
||||||
if not r.ok:
|
|
||||||
raise Exception(f"POST {path}: {r.status_code} {r.reason}: {r.text[:200]}")
|
|
||||||
result = {}
|
|
||||||
try:
|
|
||||||
parsed = r.json()
|
|
||||||
if isinstance(parsed, dict):
|
|
||||||
result = parsed
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
loc = r.headers.get("Location", "")
|
|
||||||
if loc:
|
|
||||||
result["_location"] = loc
|
|
||||||
return result
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. site/operations/tracker.py — трекер инстансов
|
|
||||||
```python
|
|
||||||
"""Трекер созданных инстансов — только те что породило приложение."""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
|
|
||||||
_LOCK = threading.Lock()
|
|
||||||
_PATH = "/tmp/instances.json"
|
|
||||||
|
|
||||||
_INITIAL = {
|
|
||||||
"408b7f96-6ed7-4985-be1b-f5bdcb6ab44d": {"svcId": 1, "displayName": "autotest-1", "instanceUid": "408b7f96-6ed7-4985-be1b-f5bdcb6ab44d"},
|
|
||||||
"884356cf-5212-4395-b56b-27c58a5fc1fa": {"svcId": 1, "displayName": "autotest-1-first", "instanceUid": "884356cf-5212-4395-b56b-27c58a5fc1fa"},
|
|
||||||
"6528854d-a4ea-428c-9fa4-68e85fa9b3ac": {"svcId": 1, "displayName": "curl-test-144834", "instanceUid": "6528854d-a4ea-428c-9fa4-68e85fa9b3ac"},
|
|
||||||
"fdcb5887-39f7-4ef8-a9c0-d55e434a55ff": {"svcId": 1, "displayName": "curl-final-test", "instanceUid": "fdcb5887-39f7-4ef8-a9c0-d55e434a55ff"},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _load():
|
|
||||||
try:
|
|
||||||
with open(_PATH) as f:
|
|
||||||
return json.load(f)
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
|
||||||
data = dict(_INITIAL)
|
|
||||||
_save(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _save(data):
|
|
||||||
with open(_PATH, "w") as f:
|
|
||||||
json.dump(data, f, indent=2)
|
|
||||||
|
|
||||||
def add(instance_uid, svc_id, display_name):
|
|
||||||
with _LOCK:
|
|
||||||
data = _load()
|
|
||||||
data[instance_uid] = {
|
|
||||||
"svcId": svc_id,
|
|
||||||
"displayName": display_name,
|
|
||||||
"instanceUid": instance_uid,
|
|
||||||
}
|
|
||||||
_save(data)
|
|
||||||
|
|
||||||
def remove(instance_uid):
|
|
||||||
with _LOCK:
|
|
||||||
data = _load()
|
|
||||||
data.pop(instance_uid, None)
|
|
||||||
_save(data)
|
|
||||||
|
|
||||||
def list_all():
|
|
||||||
with _LOCK:
|
|
||||||
return list(_load().values())
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. site/routes/api_test.py — основной файл с проблемой
|
|
||||||
```python
|
|
||||||
from flask import Blueprint, current_app, jsonify, request
|
|
||||||
|
|
||||||
from api.http_client import HttpClient
|
|
||||||
from operations.get_services import get_services, get_service_detail
|
|
||||||
from operations.get_instances import get_instances
|
|
||||||
from operations.tracker import add as tracker_add, list_all as tracker_list
|
|
||||||
from operations.tracker import remove as tracker_remove
|
|
||||||
|
|
||||||
bp = Blueprint("api_test", __name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _client():
|
|
||||||
token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
|
|
||||||
return HttpClient(current_app.config["NUBES_API_ENDPOINT"], token)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/services")
|
|
||||||
def api_services():
|
|
||||||
try:
|
|
||||||
raw = get_services(_client())
|
|
||||||
svc_list = [{"svcId": s["svcId"], "svc": s["svc"], "svcExtendedName": s.get("svcExtendedName", "")} for s in raw]
|
|
||||||
svc_list.sort(key=lambda s: s["svcId"])
|
|
||||||
return jsonify(svc_list)
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/instances/list")
|
|
||||||
def api_instances_list():
|
|
||||||
try:
|
|
||||||
raw = get_instances(_client())
|
|
||||||
inst = [{"instanceUid": i["instanceUid"], "displayName": i["displayName"], "serviceId": i["serviceId"], "svc": i["svc"], "explainedStatus": i.get("explainedStatus", "?")} for i in raw]
|
|
||||||
return jsonify(inst)
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/operations/<int:svc_id>")
|
|
||||||
def api_operations(svc_id):
|
|
||||||
try:
|
|
||||||
detail = get_service_detail(_client(), svc_id)
|
|
||||||
ops = detail.get("operations", [])
|
|
||||||
# только отслеживаемые инстансы этого сервиса
|
|
||||||
tracked = tracker_list()
|
|
||||||
tracked_uids = {t["instanceUid"] for t in tracked if t["svcId"] == svc_id}
|
|
||||||
instances = get_instances(_client())
|
|
||||||
svc_instances = [i for i in instances
|
|
||||||
if i.get("instanceUid") in tracked_uids
|
|
||||||
and i.get("explainedStatus") not in ("deleted", "not created")]
|
|
||||||
return jsonify({
|
|
||||||
"svc": detail.get("svc", ""),
|
|
||||||
"operations": [{"svcOperationId": o["svcOperationId"], "operation": o["operation"]} for o in ops],
|
|
||||||
"instances": svc_instances,
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/params/<int:op_id>")
|
|
||||||
def api_params(op_id):
|
|
||||||
try:
|
|
||||||
data = _client().get(f"/instanceOperations/default/{op_id}")
|
|
||||||
params = data["svcOperation"]["cfsParams"]
|
|
||||||
result = []
|
|
||||||
for p in params:
|
|
||||||
result.append({
|
|
||||||
"svcOperationCfsParamId": p["svcOperationCfsParamId"],
|
|
||||||
"name": p.get("svcOperationCfsParam", ""),
|
|
||||||
"dataType": p.get("dataType", ""),
|
|
||||||
"isRequired": p.get("isRequired", False),
|
|
||||||
"defaultValue": p.get("defaultValue"),
|
|
||||||
"valueList": p.get("valueList"),
|
|
||||||
"dataDescriptor": {k: {"dataType": v.get("dataType",""), "valueList": v.get("valueList",""), "isRequired": v.get("isRequired", False)} for k, v in p.get("dataDescriptor", {}).items()} if p.get("dataDescriptor") else None,
|
|
||||||
})
|
|
||||||
return jsonify(result)
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/test", methods=["POST"])
|
|
||||||
def api_test():
|
|
||||||
"""Запустить операцию — возвращает opUid сразу, выполнение в фоне."""
|
|
||||||
import threading, time
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
svc_id = data["serviceId"]
|
|
||||||
op_name = data["operation"]
|
|
||||||
svc_op_id = data["svcOperationId"]
|
|
||||||
params = data.get("params", {})
|
|
||||||
instance_uid = data.get("instanceUid")
|
|
||||||
display_name = data.get("displayName", f"autotest-{svc_id}")
|
|
||||||
|
|
||||||
client = _client()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if op_name == "create":
|
|
||||||
payload = {"serviceId": svc_id, "displayName": display_name, "descr": ""}
|
|
||||||
resp = client.post("/instances", payload)
|
|
||||||
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
|
|
||||||
if not instance_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить instanceUid"}), 500
|
|
||||||
|
|
||||||
op_payload = {"instanceUid": instance_uid, "operation": "create"}
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
|
||||||
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
|
|
||||||
if not op_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
|
|
||||||
|
|
||||||
for pid, pval in params.items():
|
|
||||||
client.post("/instanceOperationCfsParams",
|
|
||||||
{"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)})
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
|
||||||
|
|
||||||
# Записать в трекер СРАЗУ, до фонового потока
|
|
||||||
try:
|
|
||||||
tracker_add(instance_uid, svc_id, display_name)
|
|
||||||
except Exception:
|
|
||||||
pass # ← МОЛЧА ПРОГЛАТЫВАЕТ ОШИБКУ
|
|
||||||
|
|
||||||
# фоном ждать завершения
|
|
||||||
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True), daemon=True).start()
|
|
||||||
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid})
|
|
||||||
|
|
||||||
else:
|
|
||||||
if not instance_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Нет instanceUid"}), 400
|
|
||||||
|
|
||||||
if op_name == "redeploy":
|
|
||||||
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name}
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
|
||||||
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
|
|
||||||
if not op_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
|
||||||
else:
|
|
||||||
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name}
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
|
||||||
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
|
|
||||||
if not op_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
|
|
||||||
for pid, pval in params.items():
|
|
||||||
client.post("/instanceOperationCfsParams",
|
|
||||||
{"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)})
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
|
||||||
|
|
||||||
is_delete = (op_name == "delete")
|
|
||||||
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, False, is_delete), daemon=True).start()
|
|
||||||
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
return jsonify({"status": "FAIL", "error": str(e) + " | " + traceback.format_exc()[-200:]})
|
|
||||||
|
|
||||||
|
|
||||||
# Результаты фоновых операций: opUid → {status, error, stages, duration}
|
|
||||||
_op_results = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, is_delete=False):
|
|
||||||
"""Фоном ждать dtFinish и сохранить результат."""
|
|
||||||
import time
|
|
||||||
t0 = time.time()
|
|
||||||
deadline = t0 + 300
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages")
|
|
||||||
except Exception:
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
op = data.get("instanceOperation", {})
|
|
||||||
dt_finish = op.get("dtFinish")
|
|
||||||
_op_results[op_uid] = {
|
|
||||||
"status": "RUNNING",
|
|
||||||
"stages": op.get("stages", []),
|
|
||||||
"duration": round(time.time() - t0, 1),
|
|
||||||
}
|
|
||||||
if dt_finish and str(dt_finish).strip():
|
|
||||||
is_ok = op.get("isSuccessful")
|
|
||||||
err = op.get("errorLog") or ""
|
|
||||||
print(f"[DEBUG] _finish_op op_uid={op_uid} isSuccessful={is_ok!r}", flush=True)
|
|
||||||
if is_ok and is_delete:
|
|
||||||
tracker_remove(instance_uid)
|
|
||||||
_op_results[op_uid] = {
|
|
||||||
"status": "OK" if is_ok else "FAIL",
|
|
||||||
"error": str(err) if err else "",
|
|
||||||
"stages": op.get("stages", []),
|
|
||||||
"duration": round(time.time() - t0, 1),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
time.sleep(5)
|
|
||||||
_op_results[op_uid] = {"status": "TIMEOUT", "duration": round(time.time() - t0, 1)}
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/test/status/<op_uid>")
|
|
||||||
def api_test_status(op_uid):
|
|
||||||
"""Получить текущий статус операции (поллинг с UI)."""
|
|
||||||
if op_uid in _op_results:
|
|
||||||
return jsonify(_op_results[op_uid])
|
|
||||||
try:
|
|
||||||
data = _client().get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages")
|
|
||||||
op = data.get("instanceOperation", {})
|
|
||||||
dt_finish = op.get("dtFinish")
|
|
||||||
done = bool(dt_finish and str(dt_finish).strip())
|
|
||||||
return jsonify({
|
|
||||||
"status": "OK" if (done and op.get("isSuccessful")) else ("FAIL" if done else "RUNNING"),
|
|
||||||
"done": done,
|
|
||||||
"isSuccessful": op.get("isSuccessful"),
|
|
||||||
"isInProgress": op.get("isInProgress"),
|
|
||||||
"duration": op.get("duration"),
|
|
||||||
"stages": op.get("stages", []),
|
|
||||||
"errorLog": op.get("errorLog"),
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
def _find_uid(d):
|
|
||||||
if isinstance(d, dict):
|
|
||||||
for k in ("instanceUid", "instanceOperationUid", "uid", "Uid"):
|
|
||||||
if k in d:
|
|
||||||
return d[k]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _uid_from_location(loc):
|
|
||||||
parts = loc.rstrip("/").split("/")
|
|
||||||
return parts[-1] if parts else None
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. site/routes/main.py — главная страница (фрагмент с /api/operations)
|
|
||||||
```python
|
|
||||||
@bp.route("/api/operations/<int:svc_id>")
|
|
||||||
def api_operations(svc_id):
|
|
||||||
try:
|
|
||||||
detail = get_service_detail(_client(), svc_id)
|
|
||||||
ops = detail.get("operations", [])
|
|
||||||
tracked = tracker_list()
|
|
||||||
tracked_uids = {t["instanceUid"] for t in tracked if t["svcId"] == svc_id}
|
|
||||||
instances = get_instances(_client())
|
|
||||||
svc_instances = [i for i in instances
|
|
||||||
if i.get("instanceUid") in tracked_uids
|
|
||||||
and i.get("explainedStatus") not in ("deleted", "not created")]
|
|
||||||
return jsonify({
|
|
||||||
"svc": detail.get("svc", ""),
|
|
||||||
"operations": [{"svcOperationId": o["svcOperationId"], "operation": o["operation"]} for o in ops],
|
|
||||||
"instances": svc_instances,
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. site/templates/index.html — UI (ключевые функции)
|
|
||||||
```javascript
|
|
||||||
// Инстансы + кнопки операций
|
|
||||||
async function selectService(svcId){
|
|
||||||
selectedInst=null; selectedOp=null; stopPoll();
|
|
||||||
document.getElementById('params-card').style.display='none';
|
|
||||||
document.getElementById('stages-box').style.display='none';
|
|
||||||
const r=await fetch('/api/operations/'+svcId);
|
|
||||||
const d=await r.json();
|
|
||||||
svcInstances=d.instances||[];
|
|
||||||
// ... рендерит список
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeOp(params){
|
|
||||||
stopPoll();
|
|
||||||
const displayName=document.getElementById('param-displayname')?.value||'autotest-1';
|
|
||||||
// ... очищает форму
|
|
||||||
const r=await fetch('/api/test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
|
||||||
serviceId:SVC_ID,
|
|
||||||
operation:selectedOp.opName,
|
|
||||||
svcOperationId:selectedOp.opId,
|
|
||||||
params,
|
|
||||||
instanceUid:selectedInst||'',
|
|
||||||
displayName
|
|
||||||
})});
|
|
||||||
const d=await r.json();
|
|
||||||
if(d.status==='FAIL'){/* ошибка */ return;}
|
|
||||||
// start polling
|
|
||||||
const opUid=d.opUid;
|
|
||||||
pollTimer=setInterval(async()=>{
|
|
||||||
const sr=await fetch('/api/test/status/'+opUid);
|
|
||||||
const sd=await sr.json();
|
|
||||||
showStages(sd.stages||[]);
|
|
||||||
if(sd.status!=='RUNNING'){
|
|
||||||
stopPoll();
|
|
||||||
// ... показать результат
|
|
||||||
if(sd.status==='OK'){
|
|
||||||
if(selectedOp.opName==='create'){await selectService(SVC_ID);} // ← ПЕРЕЗАГРУЖАЕТ ВСЁ
|
|
||||||
else{await refreshInstances();}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshInstances(){
|
|
||||||
const r=await fetch('/api/operations/'+SVC_ID);
|
|
||||||
const d=await r.json();
|
|
||||||
svcInstances=d.instances||[];
|
|
||||||
// обновить только бейджи статусов
|
|
||||||
svcInstances.forEach(i=>{
|
|
||||||
const el=document.querySelector(`[data-iuid="${i.instanceUid}"] .badge`);
|
|
||||||
if(el){
|
|
||||||
el.textContent=i.explainedStatus||'?';
|
|
||||||
el.className='badge '+(i.explainedStatus==='running'?'badge-success':'');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ЧТО ПРОИСХОДИТ ПРИ CREATE (трассировка)
|
|
||||||
|
|
||||||
1. UI: `executeOp()` → POST `/api/test` `{serviceId:1, operation:"create", svcOperationId:18, params:{...}, instanceUid:"", displayName:"autotest-1-lq5x3a"}`
|
|
||||||
2. `api_test()`: `op_name="create"`, `svc_id=1` (int), `display_name="autotest-1-lq5x3a"` (str)
|
|
||||||
3. `client.post("/instances", ...)` → ответ от Nubes → `instance_uid = "f192b10d-beb0-4a25-b79f-5dbd7de4712e"`
|
|
||||||
4. `client.post("/instanceOperations", ...)` → `op_uid = "d489348e-5d92-47cd-97f7-25f1c4d65ffc"`
|
|
||||||
5. Параметры → `client.post("/instanceOperationCfsParams", ...)` для каждого
|
|
||||||
6. `client.post("/instanceOperations/{op_uid}/run")` → запуск
|
|
||||||
7. **`tracker_add("f192b10d-...", 1, "autotest-1-lq5x3a")`** ← ЗДЕСЬ ПРОБЛЕМА
|
|
||||||
8. `threading.Thread(target=_finish_op, ...)` → фон
|
|
||||||
9. Возврат `{status:"RUNNING", opUid:"d489348e-...", instanceUid:"f192b10d-..."}`
|
|
||||||
10. UI поллит `/api/test/status/d489348e-...`
|
|
||||||
11. `_finish_op` получает `isSuccessful=true` → `_op_results[opUid] = {status:"OK", ...}`
|
|
||||||
12. UI получает OK → вызывает `selectService(1)` → `/api/operations/1` → `tracker_list()` → 4 инстанса → **нового НЕТ**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ВОПРОСЫ
|
|
||||||
|
|
||||||
### Критический: почему tracker_add не работает?
|
|
||||||
|
|
||||||
1. Может ли `json.dump` в `/tmp/instances.json` падать молча на pythonk8s? (диск full, fs ro, quota)
|
|
||||||
2. Может ли `_save` записать, но `_load` прочитать старую версию из-за кеша ФС?
|
|
||||||
3. Может ли gunicorn preload создавать несколько копий модуля tracker.py с разными `_LOCK`?
|
|
||||||
4. Может ли `_INITIAL` перезаписывать файл при КАЖДОМ `_load`, если файл повреждён?
|
|
||||||
5. **ГЛАВНЫЙ ВОПРОС: как надёжно сохранять состояние на pythonk8s БЕЗ persistent volume?**
|
|
||||||
|
|
||||||
### Архитектурный
|
|
||||||
6. Не перейти ли на sqlite3 в `/tmp/`? Даст ли это атомарность?
|
|
||||||
7. Не заменить ли `/tmp/instances.json` на in-memory dict + seed из `_INITIAL` при старте? (без файла вообще)
|
|
||||||
8. Как правильно логировать ошибки на pythonk8s чтобы их было видно?
|
|
||||||
|
|
||||||
### UI
|
|
||||||
9. После успешного CREATE `selectService()` скрывает progress card — это бесит. Как обновить только список инстансов не скрывая stages?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Что уже пробовали (НЕ ПОМОГЛО)
|
|
||||||
|
|
||||||
- v1.0.46: tracker_add в daemon-потоке _finish_op → поток умирает под gunicorn
|
|
||||||
- v1.0.47: tracker_add в _finish_op, но до _op_results[OK] → не помогло
|
|
||||||
- v1.0.48: tracker_add синхронно в api_test() ДО потока, try/except: pass → ОШИБКА СКРЫТА
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Sonnet 4.6 — Полный код-ревью v1.1.20
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
|
|
||||||
## 🔴 КРИТИЧЕСКИЙ БАГ #1 — `_finish_op`: неправильный порядок аргументов для CREATE
|
|
||||||
|
|
||||||
Файл: /home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py
|
|
||||||
|
|
||||||
Сигнатура: `def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, client_id, stand, is_delete=False, params=None, user_email="", app_version=""):`
|
|
||||||
|
|
||||||
Вызов CREATE передаёт 13 аргументов. `params` попадает на позицию `is_delete` → dict truthy → после успешного CREATE вызывается `tracker_remove` — инстанс НЕМЕДЛЕННО удаляется из трекера.
|
|
||||||
|
|
||||||
## 🔴 ВАЖНЫЙ БАГ #2 — `api_history()`: утечка соединения
|
|
||||||
|
|
||||||
Нет `try/finally` — при исключении соединение не возвращается в пул. 5 утечек = пул исчерпан.
|
|
||||||
|
|
||||||
## 🔴 ВАЖНЫЙ БАГ #3 — pool.py: `_initialized = True` до `init_db()`
|
|
||||||
|
|
||||||
При сетевой ошибке `init_db()` не выполняется, но флаг уже True — схема не создастся до перезапуска воркера.
|
|
||||||
|
|
||||||
## 🟡 XSS — `displayName` и `svc` не экранируются в app.js
|
|
||||||
|
|
||||||
`_esc()` не применяется при рендере списка инстансов. Вектор через поле displayName.
|
|
||||||
|
|
||||||
## 🟡 Cookie без `secure=True`
|
|
||||||
|
|
||||||
## 🟡 MODIFY — не досылаются required+default параметры (в отличие от CREATE)
|
|
||||||
|
|
||||||
## 🟠 Мёртвый код: runner.py (LEGACY), дубликат redeploy/else, устаревшие docstring'и
|
|
||||||
|
|
||||||
## 🟠 pool.py — race condition при создании пула без мьютекса
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# Полный код-ревью ВСЕГО проекта autotest
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
Flask 3.0 + gunicorn (multi-worker) + vanilla JS + psycopg2 PostgreSQL.
|
|
||||||
Тестирует Nubes REST API — создаёт/удаляет инстансы облачных сервисов.
|
|
||||||
Версия: v1.1.20 (сегодня, после 20 правок за день).
|
|
||||||
За день сделано ~20 правок, накопился технический долг — нужен свежий взгляд.
|
|
||||||
|
|
||||||
## ВСЕ файлы (прочитай КАЖДЫЙ)
|
|
||||||
|
|
||||||
```
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/app.py — точка входа, регистрация blueprint'ов
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/runner.py — загрузка config.yaml
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/api/auth.py — get_token, get_client, get_client_id, get_stand
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/api/http_client.py — HttpClient, detect_endpoint, stand_name
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/db/pool.py — psycopg2 ThreadedConnectionPool (lazy)
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/db/init_db.py — CREATE TABLE runs + миграции
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/db/save_run.py — INSERT в runs (16 колонок)
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/operations/get_instances.py — GET /instances (пагинация)
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py — get_params_with_current_values, _normalize_value_list
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/operations/get_services.py — GET /services, /services/{id}
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/operations/service_list.py — load_service_ids из services_{stand}.txt
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/operations/tracker.py — fcntl.flock file tracker
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/routes/api.py — LEGACY эндпоинты
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py — ОСНОВНОЙ: /api/test, /api/params, /api/log, поллинг, _finish_op
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/routes/main.py — / (главная), /api/operations/{svc_id}
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/static/app.js — ВЕСЬ фронтенд (420 строк)
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/templates/index.html — Jinja2 шаблон
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что нужно от тебя
|
|
||||||
|
|
||||||
1. **Найти ВСЕ баги** — синтаксические, логические, race conditions, необработанные исключения
|
|
||||||
2. **Проверить универсальность** — работает ли код для ВСЕХ сервисов (Redis 3 параметра, PostgreSQL 8 map-fixed параметров, Болванка)
|
|
||||||
3. **Проверить CREATE flow** — сверь с эталонным Terraform flow из /home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md
|
|
||||||
4. **Проверить MODIFY/SUSPEND/DELETE/RESUME** — нет ли там таких же проблем
|
|
||||||
5. **Проверить фронтенд** — все ли DOM ID совпадают, нет ли необработанных Promise, правильный ли порядок элементов
|
|
||||||
6. **Проверить на дубликаты** — нет ли мёртвого кода, дублирующихся эндпоинтов
|
|
||||||
7. **Проверить безопасность** — httponly cookie, токены, CORS
|
|
||||||
|
|
||||||
## Известные недавние баги (уже исправлены, но проверь что фиксы корректны)
|
|
||||||
|
|
||||||
- `_client()` → NameError (не было такой функции) → заменили на get_client()
|
|
||||||
- `-строка` → TypeError в сортировке → заменили на два sort()
|
|
||||||
- `_normalize_value_list` не импортирован → добавили импорт
|
|
||||||
- `</div>\`;` осиротевший HTML в JS → удалили
|
|
||||||
- `currentSvcName` вместо реального `i.svc` → исправили
|
|
||||||
- serviceId фильтр (убран, заменён на сортировку)
|
|
||||||
- dataDescriptor defaultValue не прокидывался → добавили
|
|
||||||
- required+default params не досылались (шаг 6 Terraform) → добавили
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
|
|
||||||
|
|
||||||
# Sonnet — почему ты пропустил эти баги?
|
|
||||||
|
|
||||||
Отправлено: 27.07.2026, после v1.0.49 (твой анализ выполнен, НЕ ПОМОГЛО)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ты предложил 4 правки — мы сделали. Инстанс ВСЁ РАВНО не появляется в списке. Плюс новый баг: ❌ вместо ⏳ для этапов в процессе.
|
|
||||||
|
|
||||||
## Ты НЕ заметил баг №1: showStages() — ❌ для выполняющихся этапов
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function showStages(stages){
|
|
||||||
stages.forEach(s=>{
|
|
||||||
const ok=s.isSuccessful;
|
|
||||||
const icon=ok===true?'✅':ok===false?'❌':'⏳'; // ← БАГ
|
|
||||||
```
|
|
||||||
|
|
||||||
API Nubes для этапа в процессе возвращает `isSuccessful: false` (или `null`).
|
|
||||||
Код интерпретирует `false === false` → ❌ (авария).
|
|
||||||
Но этап просто **ещё не завершился** — должен быть ⏳.
|
|
||||||
|
|
||||||
**Правильная логика:** проверять `dtFinish` этапа. Если `dtFinish` нет → ⏳.
|
|
||||||
Если есть и `isSuccessful === true` → ✅. Если есть и `false` → ❌.
|
|
||||||
|
|
||||||
Почему ты это пропустил? Код `showStages()` был в твоём обзоре.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ты НЕ заметил баг №2: api_operations() теряет инстансы
|
|
||||||
|
|
||||||
```python
|
|
||||||
def api_operations(svc_id):
|
|
||||||
tracked = tracker_list() # ← 5 UID (включая новый)
|
|
||||||
tracked_uids = {t["instanceUid"] for t in tracked if t["svcId"] == svc_id}
|
|
||||||
instances = get_instances(_client()) # ← Nubes API: только 4 старых
|
|
||||||
svc_instances = [i for i in instances
|
|
||||||
if i.get("instanceUid") in tracked_uids # ← новый UID НЕ НАЙДЕН
|
|
||||||
and i.get("explainedStatus") not in ("deleted", "not created")]
|
|
||||||
```
|
|
||||||
|
|
||||||
`tracker_add` работает (in-memory, мгновенно). Но `get_instances()` из Nubes API **не сразу** возвращает только что созданный инстанс.
|
|
||||||
Фильтр требует чтобы инстанс был В ОБОИХ источниках. Новый UID есть в `tracked_uids` но отсутствует в `instances` → выпадает.
|
|
||||||
|
|
||||||
Результат: список всегда показывает только старые инстансы, новый — никогда.
|
|
||||||
|
|
||||||
**Правильная логика:** инстансы из трекера, отсутствующие в `get_instances()`, добавлять напрямую со статусом `"creating"`.
|
|
||||||
|
|
||||||
Почему ты это пропустил? Код `api_operations()` был в твоём обзоре, полностью.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Что мы имеем после твоих рекомендаций
|
|
||||||
|
|
||||||
| Версия | Что делали | Результат |
|
|
||||||
|--------|-----------|-----------|
|
|
||||||
| v1.0.48 | tracker_add синхронно, except:pass | ❌ инстанса нет |
|
|
||||||
| v1.0.49 | in-memory dict (твоя рекомендация) | ❌ инстанса нет |
|
|
||||||
| v1.0.49 | refreshInstances вместо selectService | ❌ stages ушли, но инстанса нет |
|
|
||||||
|
|
||||||
Трекер РАБОТАЕТ — in-memory dict, добавление мгновенное.
|
|
||||||
НО фильтр `api_operations()` ВЫБРАСЫВАЕТ новый инстанс потому что Nubes API его ещё не проиндексировал.
|
|
||||||
|
|
||||||
## Вопрос
|
|
||||||
|
|
||||||
Ты проанализировал ВЕСЬ код, включая `api_operations()` и `showStages()`.
|
|
||||||
Как ты мог пропустить оба этих бага, которые видны при простой трассировке CREATE-потока?
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# Полный код-ревью и вопрос: CREATE пропускает required-параметры
|
|
||||||
|
|
||||||
## Что такое autotest
|
|
||||||
|
|
||||||
Flask 3.0 + gunicorn + vanilla JS. Тестирует Nubes REST API — создаёт/удаляет инстансы облачных сервисов (Redis, PostgreSQL, S3, etc.). Всё через API: POST /instances, POST /instanceOperations, POST /instanceOperationCfsParams, POST /run.
|
|
||||||
|
|
||||||
## Текущий CREATE-флоу (код)
|
|
||||||
|
|
||||||
Файл: `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py`, функция `api_test()`, строки 209-225:
|
|
||||||
|
|
||||||
```python
|
|
||||||
display_name = _unique_display_name(client, display_name)
|
|
||||||
descr = f"created by autotest v{current_app.config.get('VERSION', '')}"
|
|
||||||
payload = {"serviceId": svc_id, "displayName": display_name, "descr": descr}
|
|
||||||
resp = client.post("/instances", payload) # шаг 1
|
|
||||||
instance_uid = resp.get("instanceUid") or _find_uid(resp) ...
|
|
||||||
|
|
||||||
op_payload = {"instanceUid": instance_uid, "operation": "create"}
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload) # шаг 2
|
|
||||||
op_uid = _find_uid(op_resp) ...
|
|
||||||
|
|
||||||
tracker_add(...)
|
|
||||||
|
|
||||||
for pid, pval in params.items(): # шаг 3 — ТОЛЬКО пользовательские
|
|
||||||
client.post("/instanceOperationCfsParams",
|
|
||||||
{"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)})
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run") # шаг 4
|
|
||||||
```
|
|
||||||
|
|
||||||
## Эталонный флоу (Terraform-провайдер)
|
|
||||||
|
|
||||||
Файл: `/home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md` (скопирован из репы tf_provider)
|
|
||||||
|
|
||||||
```
|
|
||||||
Шаг 1: POST /instances {serviceId, displayName, descr}
|
|
||||||
Шаг 2: POST /instanceOperations {instanceUid, operation:"create"}
|
|
||||||
Шаг 3: GET /instanceOperations/{opUid}?fields=cfsParams ← получить ВСЕ params с defaults
|
|
||||||
Шаг 4: resolveRefSvcParamValues ← резолв refSvcId
|
|
||||||
Шаг 5: POST /instanceOperationCfsParams ← пользовательские params (×N)
|
|
||||||
Шаг 6: POST /instanceOperationCfsParams ← required params с defaultValue, не переданные в шаге 5 (×M) ⬅ НЕТ В AUTOTEST
|
|
||||||
Шаг 7: GET /instanceOperations/{opUid}/validate-cfs
|
|
||||||
Шаг 8: POST /instanceOperations/{opUid}/run {}
|
|
||||||
Шаг 9: поллинг dtFinish
|
|
||||||
```
|
|
||||||
|
|
||||||
## Баг
|
|
||||||
|
|
||||||
PostgreSQL (сервис 90, create opId=19) имеет **8 map-fixed параметров**, все required:
|
|
||||||
- startupConfiguration (id=789)
|
|
||||||
- clusterConfiguration (id=788)
|
|
||||||
- accessConfiguration (id=790)
|
|
||||||
- postgresConfiguration (id=791)
|
|
||||||
- postgresConf (id=792) — array-map-fixed
|
|
||||||
- backupConfiguration (id=793)
|
|
||||||
- autoscaleConfiguration (id=794)
|
|
||||||
- mtlsConfiguration (id=1094)
|
|
||||||
|
|
||||||
Пользователь в UI заполняет первые 3-4. Остальные 4-5 **не отправляются** → Nubes возвращает: «Не передан параметр: serviceInstanceUid».
|
|
||||||
|
|
||||||
Redis (3 параметра), Болванка (все заполняются) — работают.
|
|
||||||
|
|
||||||
## Нужное решение
|
|
||||||
|
|
||||||
Добавить **шаг 6** в CREATE-ветку: после отправки пользовательских `params`, дослать required-параметры с их `defaultValue`, которые не были в пользовательском вводе.
|
|
||||||
|
|
||||||
## Вопросы
|
|
||||||
|
|
||||||
1. **Где брать список required-параметров с defaultValue?**
|
|
||||||
- Вариант А: сделать `GET /instanceOperations/default/{svcOperationId}` (тот же что для /api/params) — там есть все params с `isRequired` и `defaultValue`. Взять те, где `isRequired=True` и `svcOperationCfsParamId` нет в `params.keys()`.
|
|
||||||
- Вариант Б: сделать `GET /instanceOperations/{opUid}?fields=cfsParams` как Terraform (шаг 3) — после создания операции, получить cfsParams от сервера. Но это доп. запрос.
|
|
||||||
- Какой правильнее?
|
|
||||||
|
|
||||||
2. **Как отличить «пользователь заполнил» от «не заполнил»?** Сейчас params приходят из фронтенда как `{"788": "{\"cpu\":\"500\",...}"}`. Если поле не в params.keys() — значит юзер не заполнил. Но что если юзер оставил пустую строку? Достаточно проверки `pid not in params` или нужно проверять значение?
|
|
||||||
|
|
||||||
3. **Нужны ли шаги 3 (GET cfsParams) и 7 (validate)?** Terraform их делает. Без validate-cfs мы пропускаем серверную валидацию до /run. Это критично или нет?
|
|
||||||
|
|
||||||
4. **Не сломает ли это существующие сервисы?** Если добавить досылку required+default для ВСЕХ сервисов — не навредит ли это Redis/Болванке где и так всё работает?
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
# Sonnet 4.6 — Полный анализ: параметризованные операции инстанса + поиск багов
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
Версия: v1.1.36
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
Flask 3.0 + gunicorn + vanilla JS. Тестирует Nubes REST API.
|
|
||||||
|
|
||||||
PostgreSQL создался успешно. Но при попытке `create_user` — ошибка: параметры не были запрошены у пользователя, отправлен пустой `{}`.
|
|
||||||
|
|
||||||
## 🔴 Проблема 1: runOp() не показывает форму параметров для не-modify операций
|
|
||||||
|
|
||||||
**Файл:** `/home/naeel/nubes/autotest/app-autotest/site/static/app.js`, функция `runOp()`, строка 127:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function runOp(opName,opId){
|
|
||||||
if(busy) return;
|
|
||||||
stopPoll();
|
|
||||||
selectedOp={opId,opName,svcId:currentSvcId};
|
|
||||||
if(opName==='modify'){
|
|
||||||
showParams(opId,opName); // ← только modify показывает форму
|
|
||||||
}else{
|
|
||||||
// confirm and execute immediately
|
|
||||||
if(!confirm(`Запустить ${opName} для ${findInstName()}?`)) return;
|
|
||||||
executeOp({}); // ← пустые params → API ругается "Missing required parameter"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Проблема:** PostgreSQL имеет много операций с параметрами:
|
|
||||||
- `create_user` — нужен username
|
|
||||||
- `delete_user` — нужен username
|
|
||||||
- `create_database` — нужны параметры БД
|
|
||||||
- `delete_database` — нужны параметры
|
|
||||||
- `restart` — может иметь параметры
|
|
||||||
- `recovery` — параметры восстановления
|
|
||||||
- `create_backup` — параметры бэкапа
|
|
||||||
|
|
||||||
Все они сейчас идут с `executeOp({})` — пустые params.
|
|
||||||
|
|
||||||
**Операции БЕЗ параметров** (можно confirm → execute):
|
|
||||||
- `delete`
|
|
||||||
- `suspend`
|
|
||||||
- `resume`
|
|
||||||
- `redeploy`
|
|
||||||
- `reconcile`
|
|
||||||
|
|
||||||
**Вопрос:** Как универсально определить, нужны ли параметры для операции?
|
|
||||||
- Вариант А: проверять `cfsParams` в `/api/operations/{svcId}` — если есть params → показать форму
|
|
||||||
- Вариант Б: список известных "безпараметровых" операций (delete/suspend/resume/redeploy/reconcile) — для них confirm, для остальных — showParams
|
|
||||||
- Вариант В: всегда показывать форму (showParams сам разберётся если params пустые)
|
|
||||||
|
|
||||||
## 🔴 Проблема 2: refreshInstances() ДОЛЖЕН обновлять список инстансов после create_user и подобных
|
|
||||||
|
|
||||||
После `create_user` инстанс не меняется, но список инстансов всё равно надо обновить (в UI могут быть связанные изменения). Сейчас `refreshInstances()` вызывается только при `sd.status === 'OK'`.
|
|
||||||
|
|
||||||
**Файл:** `/home/naeel/nubes/autotest/app-autotest/site/static/app.js`, executeOp polling (строка ~357):
|
|
||||||
```javascript
|
|
||||||
if(sd.status==='OK'){
|
|
||||||
await refreshInstances(); // только при OK
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Вопрос:** Нужно ли вызывать refreshInstances при любом завершении (OK или FAIL)?
|
|
||||||
|
|
||||||
## Полный аудит всех оставшихся проблем
|
|
||||||
|
|
||||||
Пожалуйста, прочитай ВСЕ файлы и найди ЛЮБЫЕ оставшиеся баги или неустойчивости:
|
|
||||||
|
|
||||||
**Файлы для проверки:**
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` — весь фронтенд
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` — бэкенд
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/main.py` — главная + /api/operations
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/api/http_client.py` — HTTP-клиент
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py` — параметры
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/templates/index.html` — HTML шаблон
|
|
||||||
|
|
||||||
**Что искать:**
|
|
||||||
1. Все места где параметры операций не запрашиваются у пользователя
|
|
||||||
2. Все места где busy lock может застрять
|
|
||||||
3. Необработанные ошибки (пустые catch, пропущенные исключения)
|
|
||||||
4. Несоответствия между JS и бэкендом (разные имена полей, форматы)
|
|
||||||
5. Проблемы с `currentSvcShort` — где объявлена, где используется
|
|
||||||
6. Любые другие баги которые мы пропустили за 35+ версий
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
|
|
||||||
|
|
||||||
# Вопрос к Sonnet — архитектура трекера инстансов + UI кнопок
|
|
||||||
|
|
||||||
Отправлено: 27.07.2026
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Ты — senior архитектор. Разбери проблему в Flask-приложении на Nubes pythonk8s (gunicorn, 1 worker).
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
Приложение app-autotest — веб-интерфейс для автотестов операций облачных сервисов Nubes.
|
|
||||||
Репозиторий: https://gitea.services.ngcloud.ru/forcloud/app-autotest.git
|
|
||||||
Версия: v1.0.47
|
|
||||||
|
|
||||||
## Файлы для изучения
|
|
||||||
|
|
||||||
| Файл | Назначение |
|
|
||||||
|------|-----------|
|
|
||||||
| `site/app.py` | Точка входа Flask, VERSION, blueprints |
|
|
||||||
| `site/routes/api_test.py` | POST /api/test (запуск), GET /api/test/status (поллинг), _finish_op (фоновый поток) |
|
|
||||||
| `site/routes/main.py` | Главная страница, /api/operations/<svcId> (список инстансов из трекера) |
|
|
||||||
| `site/operations/tracker.py` | Запись/чтение /tmp/instances.json (_INITIAL, add, remove, list_all) |
|
|
||||||
| `site/templates/index.html` | UI: executeOp(), showStages(), поллинг, refreshInstances() |
|
|
||||||
| `site/api/http_client.py` | HTTP-клиент (Bearer auth, User-Agent) |
|
|
||||||
|
|
||||||
Полные пути в репозитории:
|
|
||||||
```
|
|
||||||
app-autotest/site/app.py
|
|
||||||
app-autotest/site/routes/api_test.py
|
|
||||||
app-autotest/site/routes/main.py
|
|
||||||
app-autotest/site/operations/tracker.py
|
|
||||||
app-autotest/site/templates/index.html
|
|
||||||
app-autotest/site/api/http_client.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Поток CREATE (как работает сейчас)
|
|
||||||
|
|
||||||
1. UI: пользователь жмёт «Создать» → showParams(18, 'create') → заполняет форму → executeOp(params)
|
|
||||||
2. UI отправляет POST /api/test `{serviceId:1, operation:"create", svcOperationId:18, displayName, params, instanceUid:""}`
|
|
||||||
3. Бэкенд api_test():
|
|
||||||
- `client.post("/instances", {serviceId, displayName})` → получает instanceUid
|
|
||||||
- `client.post("/instanceOperations", {instanceUid, operation:"create"})` → получает opUid
|
|
||||||
- `client.post("/instanceOperationCfsParams", ...)` — для каждого параметра
|
|
||||||
- `client.post("/instanceOperations/{opUid}/run")` — запуск
|
|
||||||
- `threading.Thread(target=_finish_op, args=(client, opUid, instanceUid, ...), daemon=True).start()`
|
|
||||||
- возвращает `{status:"RUNNING", opUid, instanceUid}`
|
|
||||||
4. UI начинает поллинг: `GET /api/test/status/<opUid>` каждые 2 секунды
|
|
||||||
5. _finish_op (фоновый daemon-поток):
|
|
||||||
- поллит `GET /instanceOperations/{opUid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages`
|
|
||||||
- обновляет `_op_results[opUid]` (in-memory dict)
|
|
||||||
- когда `dtFinish != null` и `isSuccessful == true`:
|
|
||||||
- **вызывает tracker_add(instanceUid, svcId, displayName)** ← ПРОБЛЕМА ЗДЕСЬ
|
|
||||||
- устанавливает `_op_results[opUid] = {status:"OK", stages, duration}`
|
|
||||||
- таймаут 300 секунд
|
|
||||||
6. UI при status=="OK": вызывает `selectService(SVC_ID)` → `GET /api/operations/1` → читает tracker → показывает список
|
|
||||||
|
|
||||||
## ПРОБЛЕМА
|
|
||||||
|
|
||||||
Инстанс создаётся в Nubes (виден в UI платформы), НО в списке инстансов приложения НЕ появляется.
|
|
||||||
`tracker_add` не вызывается → `/tmp/instances.json` не обновляется → инстанс не в списке.
|
|
||||||
|
|
||||||
**Происходило 2 раза подряд** (v1.0.46 и v1.0.47). Оба раза инстанс в Nubes есть, в трекере — нет.
|
|
||||||
|
|
||||||
## Моя гипотеза
|
|
||||||
|
|
||||||
tracker_add вызывается внутри daemon-потока _finish_op. В gunicorn:
|
|
||||||
- Воркер может быть перезапущен платформой в любой момент
|
|
||||||
- Daemon-потоки умирают вместе с воркером — молча, без логов
|
|
||||||
- Python не пишет traceback при смерти daemon-потока
|
|
||||||
|
|
||||||
## Текущий код _finish_op (api_test.py, строки 153-195)
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, is_delete=False):
|
|
||||||
"""Фоном ждать dtFinish и сохранить результат."""
|
|
||||||
import time
|
|
||||||
t0 = time.time()
|
|
||||||
deadline = t0 + 300
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages")
|
|
||||||
except Exception:
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
op = data.get("instanceOperation", {})
|
|
||||||
dt_finish = op.get("dtFinish")
|
|
||||||
_op_results[op_uid] = {
|
|
||||||
"status": "RUNNING",
|
|
||||||
"stages": op.get("stages", []),
|
|
||||||
"duration": round(time.time() - t0, 1),
|
|
||||||
}
|
|
||||||
if dt_finish and str(dt_finish).strip():
|
|
||||||
is_ok = op.get("isSuccessful")
|
|
||||||
err = op.get("errorLog") or ""
|
|
||||||
# Сначала трекер — чтобы UI при poll уже видел инстанс
|
|
||||||
if is_ok:
|
|
||||||
if is_create:
|
|
||||||
tracker_add(instance_uid, svc_id, display_name)
|
|
||||||
elif is_delete:
|
|
||||||
tracker_remove(instance_uid)
|
|
||||||
_op_results[op_uid] = {
|
|
||||||
"status": "OK" if is_ok else "FAIL",
|
|
||||||
"error": str(err) if err else "",
|
|
||||||
"stages": op.get("stages", []),
|
|
||||||
"duration": round(time.time() - t0, 1),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
time.sleep(5)
|
|
||||||
_op_results[op_uid] = {"status": "TIMEOUT", "duration": round(time.time() - t0, 1)}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Вопросы
|
|
||||||
|
|
||||||
### 1. Где вызывать tracker_add?
|
|
||||||
|
|
||||||
Вариант А: синхронно в api_test(), сразу после получения instanceUid от API (шаг 3), до запуска потока.
|
|
||||||
- Плюс: гарантированная запись, инстанс в трекере мгновенно
|
|
||||||
- Минус: если операция потом упадёт — в трекере «мусорный» инстанс (но это лучше чем отсутствие)
|
|
||||||
|
|
||||||
Вариант Б: subprocess.Popen вместо threading.Thread.
|
|
||||||
- Плюс: процесс живёт независимо от gunicorn worker
|
|
||||||
- Минус: сложнее, нужен IPC для _op_results
|
|
||||||
|
|
||||||
Вариант В: sqlite3 с WAL-режимом.
|
|
||||||
- Плюс: атомарная запись, конкурентный доступ
|
|
||||||
- Минус: без persistent volume данные теряются при редеплое (как и /tmp/)
|
|
||||||
|
|
||||||
### 2. Что делать с _op_results?
|
|
||||||
|
|
||||||
Сейчас это module-level dict. При нескольких gunicorn workers — каждый worker имеет свой dict. Нужен ли переход на sqlite/file-based storage для _op_results?
|
|
||||||
|
|
||||||
### 3. Надёжность под gunicorn
|
|
||||||
|
|
||||||
Как правильно организовать фоновую работу под gunicorn на pythonk8s (1 worker, нет PV, нет Redis/RabbitMQ)?
|
|
||||||
|
|
||||||
## Дополнительно: UI кнопок
|
|
||||||
|
|
||||||
Нужен CSS чтобы кнопки операций шли горизонтально в 1-2 ряда, как в Nubes UI:
|
|
||||||
|
|
||||||
```
|
|
||||||
delete | modify | suspend | resume | redeploy | reconcile
|
|
||||||
```
|
|
||||||
|
|
||||||
Сейчас они в vertical списке внутри `div.inst-ops`. Предложи стиль.
|
|
||||||
|
|
||||||
## Ограничения платформы
|
|
||||||
|
|
||||||
- `site/` — НЕ пакет (без __init__.py, конфликт с stdlib site.py)
|
|
||||||
- Импорты без префикса site.: `from api.http_client import ...`
|
|
||||||
- `app.run(host="0.0.0.0", port=5000)` — обязательно
|
|
||||||
- Gunicorn запускается платформой, предположительно 1 worker
|
|
||||||
- Нет persistent volume — данные в /tmp/ теряются при редеплое
|
|
||||||
- Деплой: git push → managed service подхватывает → редеплой
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Sonnet 4.6 — Финальный аудит
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
|
|
||||||
## 5 багов в app.js
|
|
||||||
|
|
||||||
| # | Баг | Строка |
|
|
||||||
|---|-----|--------|
|
|
||||||
| 🔴1 | `d.operations.filter` без null-guard | toggleInstance |
|
|
||||||
| 🔴2 | toggleInstance без try/catch | toggleInstance |
|
|
||||||
| 🔴3 | selectService без try/catch | selectService |
|
|
||||||
| 🔴4 | refreshInstances без try/catch | refreshInstances |
|
|
||||||
| 🟡5 | CMDB OK + refreshInstances → ошибка перезаписывает успех | executeOp |
|
|
||||||
|
|
||||||
## redeploy без _send_params_terraform
|
|
||||||
Намеренно — redeploy переиспользует текущие params из state.
|
|
||||||
|
|
||||||
## Бэкенд чист
|
|
||||||
api_test.py, main.py, get_params.py, http_client.py — багов нет.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Sonnet 4.6 — Параметры операций + аудит багов
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
|
|
||||||
## 🔴 Баг 1: runOp() — форма только для modify
|
|
||||||
app.js:127 — все кроме modify идут с executeOp({}). create_user, delete_user и др. получают пустые params.
|
|
||||||
|
|
||||||
**Решение (Вариант C):** Всегда showParams. Если params пустые — показать заголовок + кнопку без полей.
|
|
||||||
|
|
||||||
## 🔴 Баг 2: CMDB-delete не чистит трекер
|
|
||||||
api_test.py — после успешного CMDB инстанс в трекере → показывается как "creating" вечно.
|
|
||||||
|
|
||||||
## 🟠 Баг 3: Сетевая ошибка в поллинге → busy навсегда
|
|
||||||
app.js — fetch в setInterval без try/catch → silent reject → busy stuck.
|
|
||||||
|
|
||||||
## 🟡 Баг 4: refreshInstances только при OK
|
|
||||||
app.js:357 — после FAIL список не обновляется.
|
|
||||||
|
|
||||||
## 🟢 Баг 5: currentSvcId=1 хардкод
|
|
||||||
Первый сервис ≠ 1 → рассинхрон UI и данных.
|
|
||||||
|
|
||||||
## 🟢 Баг 6: detect_endpoint ×3-4 на запрос
|
|
||||||
auth.py — кэшировать через flask.g.
|
|
||||||
|
|
||||||
## Приоритет
|
|
||||||
1. Баг 2 — 1 строка
|
|
||||||
2. Баг 1 — основная задача (форма для всех операций)
|
|
||||||
3. Баг 3 — busy stuck
|
|
||||||
4. Баг 4 — одна строка
|
|
||||||
5. Баги 5-6 — отдельно
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Sonnet 4.6 — Round 2 Response
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
|
|
||||||
## Ответы
|
|
||||||
|
|
||||||
### 1. Фильтр serviceId ✅
|
|
||||||
Добавить `if i.get("serviceId") != svc_id: continue` — безопасно.
|
|
||||||
Tracker-fallback уже отфильтрован по svc_id, не сломается.
|
|
||||||
Даже улучшит дедупликацию cloud_names.
|
|
||||||
|
|
||||||
### 2. Генерация 3 символов
|
|
||||||
Надёжнее: `(Math.random() * 46656 | 0).toString(36).padStart(3, '0')`
|
|
||||||
Ровно 3 символа, равномерно по 46 656 комбинациям.
|
|
||||||
|
|
||||||
### 3. descr
|
|
||||||
Не нужно тащить через JS! `app_version` уже есть в той же функции.
|
|
||||||
Одна строка: `"descr": f"created by autotest v{app_version}"`
|
|
||||||
|
|
||||||
### 4. Пропущенный баг: stand-ключ трекера
|
|
||||||
main.py → `stand_name(endpoint)`
|
|
||||||
api_test.py → `get_stand()`
|
|
||||||
Могут разойтись → tracker_add и tracker_list под разными ключами → creating инстансы не видны.
|
|
||||||
|
|
||||||
### 5. Дубликат эндпоинта
|
|
||||||
Удалить `/api/operations/{svc_id}` из api_test.py (строки 108-148).
|
|
||||||
Недостижим, старый код, техдолг.
|
|
||||||
|
|
||||||
## Итоговый план исправлений
|
|
||||||
|
|
||||||
| # | Файл | Что |
|
|
||||||
|---|------|-----|
|
|
||||||
| 1 | main.py:194 | `if i.get("serviceId") != svc_id: continue` |
|
|
||||||
| 2 | main.py:227 | `"svcShort": detail.get("svcShort", "")` |
|
|
||||||
| 3 | app.js:53 | `currentSvcShort = d.svcShort\|\|''` |
|
|
||||||
| 4 | app.js:113-116 | Переписать `makeCreateDisplayName()` |
|
|
||||||
| 5 | api_test.py:212 | `f"created by autotest v{app_version}"` |
|
|
||||||
| 6 | api_test.py:108-148 | Удалить дубликат эндпоинта |
|
|
||||||
| 7 | main.py + api_test.py | Унифицировать stand |
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Sonnet 4.6 — Root Cause: serviceInstanceUid
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
|
|
||||||
## Найдена корневая причина
|
|
||||||
|
|
||||||
**Разница в источнике данных для шага 6 (required+default params):**
|
|
||||||
|
|
||||||
| | Terraform | Autotest |
|
|
||||||
|---|---|---|
|
|
||||||
| Источник | `GET /instanceOperations/{opUid}?fields=cfsParams` — **реальная операция** | `GET /instanceOperations/default/{svc_op_id}` — **шаблон** |
|
|
||||||
| Данные | `paramValue` (автоматически заполнен Nubes) + `defaultValue` | Только `defaultValue` |
|
|
||||||
|
|
||||||
`serviceInstanceUid` — параметр, который Nubes **автоматически добавляет** в реальную операцию (шаг 3 Terraform). В шаблоне его НЕТ. Поэтому наш код его не видит → не отправляет → validate-cfs падает.
|
|
||||||
|
|
||||||
## Исправление
|
|
||||||
|
|
||||||
В `api_test.py` CREATE-ветка (строка ~210): заменить
|
|
||||||
```python
|
|
||||||
tmpl = client.get(f"/instanceOperations/default/{svc_op_id}")
|
|
||||||
```
|
|
||||||
на
|
|
||||||
```python
|
|
||||||
op_details = client.get(f"/instanceOperations/{op_uid}?fields=cfsParams")
|
|
||||||
```
|
|
||||||
И брать `paramValue` первым, `defaultValue` как fallback — как Terraform.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Sonnet 4.6 — state.out valueList fix
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
|
|
||||||
## Ответ
|
|
||||||
|
|
||||||
### Решение верное, но с ошибкой
|
|
||||||
- `"user" in code` → `state_out["users"]` ✅
|
|
||||||
- `"owner" in code` → `state_out["users"]` ✅ (НЕ databases!)
|
|
||||||
- `"db" in code` → `state_out["databases"]` ✅
|
|
||||||
|
|
||||||
### Попутный баг
|
|
||||||
MERGED-путь (get_params.py) не нормализует `valueList` через `_normalize_value_list`. DEFAULT-путь (api_test.py) — нормализует.
|
|
||||||
|
|
||||||
## План (2 изменения в get_params.py)
|
|
||||||
|
|
||||||
1. Читать `state_out`, заполнять пустые `valueList`: user/owner→users, db→databases
|
|
||||||
2. Нормализовать `value_list` через `_normalize_value_list()`
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Sonnet 4.6 — Code Review v1.1.11
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
Модель: Claude Sonnet 4.6
|
|
||||||
|
|
||||||
## Ответ
|
|
||||||
|
|
||||||
### 🔴 Критично: NameError в api_test.py
|
|
||||||
|
|
||||||
Строки 159, 178, 180 вызывают:
|
|
||||||
- `get_params_with_current_values(...)`
|
|
||||||
- `_normalize_value_list(...)`
|
|
||||||
|
|
||||||
Оба символа определены в `operations/get_params.py`, но **не импортированы** в `routes/api_test.py`.
|
|
||||||
|
|
||||||
Цепочка падения:
|
|
||||||
1. `startCreate()` → `fetch('/api/params/{opId}')`
|
|
||||||
2. Сервер: `_normalize_value_list(p.get("valueList"))` → `NameError`
|
|
||||||
3. `except` → `{"error": "name '_normalize_value_list' is not defined"}`, HTTP 500
|
|
||||||
4. JS: `data.params` = undefined → fallback к `data` = `{"error": "..."}`
|
|
||||||
5. `params.map(...)` → `TypeError: params.map is not a function`
|
|
||||||
6. Нет `.catch()` → промис падает молча
|
|
||||||
|
|
||||||
Также сломаны MODIFY-запросы (строка 159 — `get_params_with_current_values`).
|
|
||||||
|
|
||||||
**Фикс:** одна строка импорта.
|
|
||||||
|
|
||||||
### 🟡 Вторичные баги в JS
|
|
||||||
|
|
||||||
1. `showParams()` — нет `.catch()` на fetch-цепочке → ошибки скрыты
|
|
||||||
2. `data.params||data` без `Array.isArray()` → TypeError при ошибке API
|
|
||||||
|
|
||||||
### ⚪ Косметика
|
|
||||||
|
|
||||||
`create-btn-area` после `params-area` в DOM — неудобно, но не ломает.
|
|
||||||
|
|
||||||
## Действия
|
|
||||||
|
|
||||||
- [x] Добавить импорт в api_test.py
|
|
||||||
- [x] Исправить клиентский код
|
|
||||||
- [x] Добавить `.catch()` в showParams
|
|
||||||
- [x] Проверить Array.isArray перед .map()
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Sonnet 4.6 — Round 2: оставшиеся баги и план исправлений
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
Контекст: v1.1.12 уже починил NameError и JS error handling. Сейчас v1.1.13 (мелкие UI-фиксы).
|
|
||||||
|
|
||||||
## Баги к исправлению
|
|
||||||
|
|
||||||
### 🔴 Баг 1: Инстансы всех сервисов показываются под любым выбранным
|
|
||||||
|
|
||||||
**Файл:** `/home/naeel/nubes/autotest/app-autotest/site/routes/main.py`, строка 193-194, функция `api_operations(svc_id)`
|
|
||||||
|
|
||||||
```python
|
|
||||||
for i in instances:
|
|
||||||
display_name = str(i.get("displayName", "") or "")
|
|
||||||
if not display_name.startswith(AUTOTEST_PREFIX):
|
|
||||||
continue # не наш инстанс
|
|
||||||
# ← НЕТ проверки i.get("serviceId") != svc_id
|
|
||||||
```
|
|
||||||
|
|
||||||
Фильтр только по префиксу `autotest-`, без проверки что `instance.serviceId == svc_id`.
|
|
||||||
Результат: под сервисом «Container Registry» показываются S3 бакеты, под «Redis» — Flask инстансы.
|
|
||||||
|
|
||||||
**Предлагаемый фикс:** добавить `if i.get("serviceId") != svc_id: continue`
|
|
||||||
|
|
||||||
### 🟡 Задача 2: displayName → autotest-<svcShort>-<3rand>
|
|
||||||
|
|
||||||
Сейчас: `autotest-ms4gzydq` (timestamp base36)
|
|
||||||
Нужно: `autotest-redis-x7k`
|
|
||||||
|
|
||||||
**Данные:** API `/services/{id}` возвращает `svcShort=redis`
|
|
||||||
|
|
||||||
**План:**
|
|
||||||
1. `/home/naeel/nubes/autotest/app-autotest/site/routes/main.py` `/api/operations/{svc_id}` — добавить `svcShort` в JSON-ответ
|
|
||||||
2. `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` `selectService()` — сохранить `currentSvcShort = d.svcShort`
|
|
||||||
3. `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` `makeCreateDisplayName()` — `autotest-${currentSvcShort}-${3rand}`
|
|
||||||
4. Коллизии обрабатываются в `_unique_display_name()` на бэкенде
|
|
||||||
|
|
||||||
### 🟡 Задача 3: description (descr) при создании
|
|
||||||
|
|
||||||
HAR-файл (`development/dummycreate.har`) показывает что Nubes API принимает `descr`:
|
|
||||||
```json
|
|
||||||
POST /instances
|
|
||||||
{"serviceId":1, "displayName":"dummy-11255555", "descr":""}
|
|
||||||
```
|
|
||||||
|
|
||||||
В autotest `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` строка 212 уже шлёт `"descr": ""` — пустое. Нужно заполнить:
|
|
||||||
```
|
|
||||||
"descr": "created by autotest v1.1.13"
|
|
||||||
```
|
|
||||||
|
|
||||||
**План:**
|
|
||||||
1. `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` `executeOp()` — передать `descr` в тело запроса
|
|
||||||
2. `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` `/api/test` — принять `descr` из JSON, подставить в payload
|
|
||||||
|
|
||||||
## Вопросы к Sonnet
|
|
||||||
|
|
||||||
1. Верен ли план для бага 1? Не сломает ли фильтр по serviceId tracker-fallback логику?
|
|
||||||
2. Для задачи 2: как лучше генерировать 3 случайных символа? `Math.random().toString(36).slice(2,5)`? Не будет ли коллизий?
|
|
||||||
3. Для задачи 3: `descr` уже есть в payload. Достаточно ли просто подставить строку, или нужно что-то ещё?
|
|
||||||
4. Есть ли ещё какие-то баги, которые мы упустили?
|
|
||||||
5. `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` содержит ДУБЛИКАТ эндпоинта `/api/operations/{svc_id}` (строка 108-148), который никогда не вызывается из-за порядка регистрации blueprint'ов. Удалить ли его, или он нужен для чего-то?
|
|
||||||
|
|
||||||
**Файлы для проверки (только эти):**
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/main.py` (строки 155-230)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` (строки 108-148, 195-280)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` (строки 85-130, 240-270)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Sonnet 4.6 — Round 3: missing required params in CREATE
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
Версия: v1.1.18
|
|
||||||
|
|
||||||
## Проблема
|
|
||||||
PostgreSQL create падает с ошибкой Nubes: «Не передан параметр: serviceInstanceUid».
|
|
||||||
Redis работает, Болванка работает, PG — нет.
|
|
||||||
|
|
||||||
## Что нашли
|
|
||||||
Terraform-провайдер делает дополнительный шаг после отправки пользовательских параметров:
|
|
||||||
отправляет ВСЕ required-параметры с их defaultValue, даже если юзер их не заполнял.
|
|
||||||
|
|
||||||
Наш код (api_test.py /api/test, CREATE-ветка, примерно строка 230):
|
|
||||||
```python
|
|
||||||
for pid, pval in params.items():
|
|
||||||
client.post("/instanceOperationCfsParams",
|
|
||||||
{"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)})
|
|
||||||
```
|
|
||||||
Шлёт ТОЛЬКО то, что пользователь заполнил в форме.
|
|
||||||
|
|
||||||
## PG имеет 8 map-fixed параметров, все required:
|
|
||||||
- startupConfiguration (id=789)
|
|
||||||
- clusterConfiguration (id=788)
|
|
||||||
- accessConfiguration (id=790)
|
|
||||||
- postgresConfiguration (id=791)
|
|
||||||
- postgresConf (id=792) — array-map-fixed
|
|
||||||
- backupConfiguration (id=793)
|
|
||||||
- autoscaleConfiguration (id=794)
|
|
||||||
- mtlsConfiguration (id=1094)
|
|
||||||
|
|
||||||
Если пользователь заполнил только первые 3 — остальные 5 НЕ отправляются → Nubes ругается.
|
|
||||||
|
|
||||||
## Вопросы
|
|
||||||
1. Как лучше реализовать досылку required-параметров с defaultValue?
|
|
||||||
- Вариант А: до отправки params сделать GET /instanceOperations/default/{svcOpId}, взять все required с их defaultValue, смержить с пользовательскими
|
|
||||||
- Вариант Б: после отправки пользовательских params, дослать оставшиеся required с defaultValue
|
|
||||||
2. Terraform делает GET /instanceOperations/{opUid}?fields=cfsParams ПОСЛЕ создания операции. Нужно ли нам тоже?
|
|
||||||
3. Не сломает ли это существующие сервисы (Redis, Болванка) где параметров мало и все заполняются?
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Sonnet Code Review — app-autotest v1.1.11
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
Flask 3.0 + gunicorn (multi-worker) + vanilla JS фронтенд. Тестирует Nubes API (создание/удаление инстансов).
|
|
||||||
|
|
||||||
## Файлы для проверки (только эти, не надо лазить по всей репе)
|
|
||||||
|
|
||||||
1. **Фронтенд (основной баг!)**
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` — ВСЁ
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/templates/index.html` — структура DOM
|
|
||||||
|
|
||||||
2. **Бэкенд**
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` — эндпоинты /api/params, /api/test
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/main.py` — эндпоинт /api/operations/<svc_id>
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py` — _normalize_value_list
|
|
||||||
|
|
||||||
3. **API-документация (для понимания что отдаёт Nubes)**
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/api-access.md`
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/api-create-flow.md`
|
|
||||||
|
|
||||||
## Конкретный баг
|
|
||||||
Кнопка "+ Создать новый инстанс" (функция `startCreate()` в app.js) не работает:
|
|
||||||
- При клике — `params-area` открывается пустой (display:block)
|
|
||||||
- Загрузка параметров молча не происходит
|
|
||||||
- Кнопка остаётся на месте, юзер думает что ничего не случилось
|
|
||||||
|
|
||||||
## Что нужно от Sonnet
|
|
||||||
1. Найти КОНКРЕТНУЮ причину почему `showParams` не рендерит форму. Проверить ВСЕ пути выполнения:
|
|
||||||
- `startCreate()` → fetch /api/operations/{svcId} → find create op → showParams(opId,'create')
|
|
||||||
- `showParams()` → fetch /api/params/{opId} → рендер createHeader + form
|
|
||||||
2. Нет ли race condition: `currentSvcName` может быть не установлен?
|
|
||||||
3. Нет ли ошибки в `params.map()` если API возвращает ошибку вместо массива?
|
|
||||||
4. `create-btn-area` сейчас ПОСЛЕ `params-area` в DOM — это нормально?
|
|
||||||
|
|
||||||
## Версия для справки
|
|
||||||
v1.1.11 — сегодняшние изменения: нормализация valueList CSV→array для вложенных dataDescriptor параметров.
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
# Sonnet 4.6 — valueList из state.out для внутренних операций инстанса
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
Версия: v1.1.38
|
|
||||||
|
|
||||||
## Проблема
|
|
||||||
|
|
||||||
PostgreSQL создан. `create_user` отработал успешно (HTTP 201). Но при вызове `delete_user` — поле `username` показывает пустой выпадающий список (`valueList=[]`). Созданный юзер `uuu8` не виден.
|
|
||||||
|
|
||||||
## Что нашли
|
|
||||||
|
|
||||||
**GET /instances/{uid}** возвращает `state.out` с актуальными данными:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"state": {
|
|
||||||
"out": {
|
|
||||||
"users": {"uuu8": {"username": "uuu8", "role": "ddl_user", "rights": ["createdb", "createrole"]}},
|
|
||||||
"databases": {},
|
|
||||||
"backups": [],
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Шаблон** (`/instanceOperations/default/244` — delete_user) возвращает `valueList=[]`.
|
|
||||||
|
|
||||||
**Текущий код** — файл `/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
def get_params_with_current_values(client, op_id, instance_uid):
|
|
||||||
inst_data = client.get(f"/instances/{instance_uid}")
|
|
||||||
state_params = inst_data.get("instance", {}).get("state", {}).get("params", {}) or {}
|
|
||||||
|
|
||||||
tmpl_data = client.get(f"/instanceOperations/default/{op_id}")
|
|
||||||
tmpl_params = tmpl_data.get("svcOperation", {}).get("cfsParams", []) or []
|
|
||||||
|
|
||||||
for p in tmpl_params:
|
|
||||||
...
|
|
||||||
result.append({
|
|
||||||
...
|
|
||||||
"valueList": value_list, # ← из шаблона, всегда []
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Читает `state.params` (конфигурация кластера), но НЕ читает `state.out` (пользователи, базы).
|
|
||||||
|
|
||||||
## Предлагаемое решение
|
|
||||||
|
|
||||||
После получения `inst_data`, извлечь `state.out`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
state_out = inst_data.get("instance", {}).get("state", {}).get("out", {}) or {}
|
|
||||||
```
|
|
||||||
|
|
||||||
В цикле слияния, если `valueList` пустой — попробовать заполнить из `state.out`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
value_list = p.get("valueList")
|
|
||||||
# Заполнить пустые valueList из state.out
|
|
||||||
if value_list is not None and not value_list:
|
|
||||||
code = p.get("svcOperationCfsParam", "").lower()
|
|
||||||
if "user" in code and isinstance(state_out.get("users"), dict):
|
|
||||||
value_list = sorted(state_out["users"].keys())
|
|
||||||
elif ("db" in code or "owner" in code) and isinstance(state_out.get("databases"), dict):
|
|
||||||
value_list = sorted(state_out["databases"].keys())
|
|
||||||
```
|
|
||||||
|
|
||||||
## Вопросы
|
|
||||||
|
|
||||||
1. **Правильный ли подход?** Смотреть в `state.out.users` / `state.out.databases` для заполнения пустых valueList? Или есть другой источник?
|
|
||||||
|
|
||||||
2. **Какие ещё поля из state.out могут понадобиться?** Для других сервисов (MongoDB, Redis, Kafka) — есть ли там аналогичные подресурсы?
|
|
||||||
|
|
||||||
3. **Универсальность:** Достаточно ли проверки по имени параметра (`"user" in code`, `"db" in code`)? Или нужен более общий механизм (например, проверять все ключи state.out и матчить по имени)?
|
|
||||||
|
|
||||||
4. **Кеширование:** `get_params_with_current_values` вызывается при каждом открытии формы. GET /instances/{uid} — дорогой запрос. Стоит ли кешировать state.out?
|
|
||||||
|
|
||||||
5. **Есть ли другие баги** в этой же области, которые мы упустили?
|
|
||||||
|
|
||||||
## Файлы для анализа
|
|
||||||
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py` — основной файл
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` — api_params (строка 120-155)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` — showParams (строка 145-260)
|
|
||||||
- Пример API-ответа: `curl -H "Authorization: Bearer $TOKEN" /instances/0a42bef3-7ce1-42b7-aa80-4274c4d9568f`
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Sonnet 4.6 — СВЕРКА С ТЕРРАФОРМОМ: почему autotest не работает а Terraform работает
|
|
||||||
|
|
||||||
## СУТЬ ПРОБЛЕМЫ
|
|
||||||
|
|
||||||
Terraform-провайдер создаёт PostgreSQL БЕЗ ошибок. Autotest (наш код) — падает с «Не передан параметр: serviceInstanceUid». Мы скопировали «похожую» логику, но где-то расхождение. Нужно найти ВСЕ расхождения, пошагово.
|
|
||||||
|
|
||||||
## ЭТАЛОН — Terraform CREATE flow
|
|
||||||
|
|
||||||
Файл: /home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md (читай ВЕСЬ, особенно строки 30-90)
|
|
||||||
|
|
||||||
```
|
|
||||||
Шаг 1: POST /instances {serviceId, displayName, descr}
|
|
||||||
Шаг 2: POST /instanceOperations {instanceUid, operation:"create"}
|
|
||||||
Шаг 3: GET /instanceOperations/{opUid}?fields=cfsParams ← получить ВСЕ params с defaults
|
|
||||||
Шаг 4: resolveRefSvcParamValues ← резолв refSvcId параметров
|
|
||||||
Шаг 5: POST /instanceOperationCfsParams ← пользовательские params (×N)
|
|
||||||
Шаг 6: POST /instanceOperationCfsParams ← required params с defaultValue, НЕ переданные в шаге 5 (×M)
|
|
||||||
Шаг 7: GET /instanceOperations/{opUid}/validate-cfs
|
|
||||||
Шаг 8: POST /instanceOperations/{opUid}/run {}
|
|
||||||
Шаг 9: поллинг dtFinish
|
|
||||||
```
|
|
||||||
|
|
||||||
## НАШ КОД — CREATE flow
|
|
||||||
|
|
||||||
Файл: /home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py, функция api_test(), строки 170-245
|
|
||||||
|
|
||||||
Прочитай ВЕСЬ файл, особенно:
|
|
||||||
- Строки 170-195 — создание instance + operation
|
|
||||||
- Строки 207-245 — отправка params + досылка required+default + validate + run
|
|
||||||
|
|
||||||
## HAR-файл — реальные API-вызовы Terraform
|
|
||||||
|
|
||||||
Файл: /home/naeel/nubes/autotest/development/dummycreate.har
|
|
||||||
Это HAR с реальными запросами Terraform при создании Болванки (сервис 1).
|
|
||||||
Найди ВСЕ POST /instanceOperationCfsParams — посмотри КАКИЕ именно params отправляются, в КАКОМ порядке, с КАКИМИ svcOperationCfsParamId.
|
|
||||||
|
|
||||||
## PostgreSQL YAML — структура параметров
|
|
||||||
|
|
||||||
Файл: /home/naeel/nubes/autotest/STANDS/dev/resources_yaml/90_postgres.yaml
|
|
||||||
Читай строки с operations.create.params — там 8 map-fixed параметров с sub_params.
|
|
||||||
|
|
||||||
Также проверь через API что реально возвращается:
|
|
||||||
```
|
|
||||||
curl -H "Authorization: Bearer <TOKEN>" /instanceOperations/default/19
|
|
||||||
```
|
|
||||||
(create opId для PostgreSQL = 19)
|
|
||||||
|
|
||||||
## ЧТО НУЖНО НАЙТИ
|
|
||||||
|
|
||||||
1. **ШАГ 3 Terraform vs наш код**: Terraform делает GET /instanceOperations/{opUid}?fields=cfsParams ПОСЛЕ создания операции чтобы получить параметры С РЕЗОЛВОМ refSvcId. Мы берём из /instanceOperations/default/{svc_op_id} (шаблон). В чём разница? Может ли шаблон не содержать нужных значений которые появляются только в реальной операции?
|
|
||||||
|
|
||||||
2. **ШАГ 4 Terraform vs наш код**: resolveRefSvcParamValues — что это делает? Как Terraform резолвит refSvcId (например backupConfiguration.s3Uid ссылается на serviceId=12)? Есть ли у нас такой резолв?
|
|
||||||
|
|
||||||
3. **ШАГ 6 Terraform vs наш код**: Terraform досылает required+default. Мы тоже (v1.1.19). НО: Terraform берёт defaults из cfsParams РЕАЛЬНОЙ операции (шаг 3), а не из шаблона. Может ли быть расхождение?
|
|
||||||
|
|
||||||
4. **ПОРЯДОК отправки params**: В HAR, в каком порядке идут POST /instanceOperationCfsParams? Важен ли порядок?
|
|
||||||
|
|
||||||
5. **serviceInstanceUid**: Этого параметра НЕТ ни в YAML, ни в шаблоне API. Откуда он берётся? Может это внутренний параметр Nubes который появляется при резолве refSvcId?
|
|
||||||
|
|
||||||
6. **ПРОВЕРЬ ВЕСЬ НАШ КОД** на предмет любых других расхождений с Terraform flow. Не только CREATE — MODIFY, DELETE, SUSPEND, RESUME тоже.
|
|
||||||
|
|
||||||
## ИСХОДНИКИ TERRAFORM-ПРОВАЙДЕРА (читать только эти файлы!)
|
|
||||||
|
|
||||||
Это Go-код Terraform-провайдера Nubes. В нём ЭТАЛОННАЯ логика которая РАБОТАЕТ.
|
|
||||||
|
|
||||||
- /home/naeel/tf_provider/provider/internal/core/client.go — **главный**: HTTP-клиент, CreateGenericInstanceUniversalV6 (весь CREATE flow), RunInstanceOperationUniversal (MODIFY/DELETE/SUSPEND), doRequest (retry, заголовки)
|
|
||||||
- /home/naeel/tf_provider/provider/internal/core/refsvc_resolve.go — **resolveRefSvcParamValues**: как резолвятся refSvcId (s3Uid → UUID S3-инстанса)
|
|
||||||
- /home/naeel/tf_provider/provider/internal/core/instance_params.go — обработка параметров
|
|
||||||
|
|
||||||
## Файлы autotest (наш код — сравнивать с Terraform)
|
|
||||||
|
|
||||||
- /home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py — ВЕСЬ: /api/test, /api/params, _finish_op
|
|
||||||
- /home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py — get_params_with_current_values, _normalize_value_list
|
|
||||||
- /home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md — эталонный flow (уже скопирован из client.go)
|
|
||||||
- /home/naeel/nubes/autotest/development/dummycreate.har — реальные запросы Terraform
|
|
||||||
- /home/naeel/nubes/autotest/STANDS/dev/resources_yaml/90_postgres.yaml — PG параметры
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
|
|
||||||
|
|
||||||
# Пошаговый аудит кода — 27.07.2026 (v1.0.50)
|
|
||||||
|
|
||||||
## Методология
|
|
||||||
|
|
||||||
Полная трассировка CREATE-потока: UI → api_test() → _finish_op() → api_test_status() → refreshInstances() → api_operations(). Каждый if, try, except, присваивание.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Результат: найдено 3 бага
|
|
||||||
|
|
||||||
### 🔴 Баг #1 (КРИТИЧЕСКИЙ): Multi-worker gunicorn — in-memory dict не работает
|
|
||||||
|
|
||||||
**Файл:** `site/operations/tracker.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
_data = {
|
|
||||||
"408b7f96-...": {...}, # 4 initial items
|
|
||||||
}
|
|
||||||
|
|
||||||
def add(instance_uid, svc_id, display_name):
|
|
||||||
with _LOCK:
|
|
||||||
_data[instance_uid] = {...}
|
|
||||||
```
|
|
||||||
|
|
||||||
`_data` — module-level dict. У каждого gunicorn worker своя копия модуля → свой `_data`.
|
|
||||||
|
|
||||||
**Трассировка:**
|
|
||||||
1. POST /api/test → попадает на **воркер A**
|
|
||||||
2. `tracker_add(...)` → `_data` воркера A = 5 элементов ✅
|
|
||||||
3. GET /api/operations/1 → попадает на **воркер B**
|
|
||||||
4. `tracker_list()` → `_data` воркера B = 4 элемента ❌
|
|
||||||
|
|
||||||
**Почему это объясняет ВСЕ симптомы:**
|
|
||||||
- После F5 — рандомный воркер → опять 4
|
|
||||||
- v1.0.46-1.0.49 — ни одно решение не помогало (in-memory принципиально не跨-process)
|
|
||||||
- Инстанс в Nubes есть, в трекере нет — разные воркеры
|
|
||||||
|
|
||||||
**Решение:** вернуть файловый трекер (`/tmp/instances.json`) с межпроцессной блокировкой (`fcntl.flock` вместо `threading.Lock`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔴 Баг #2 (КРИТИЧЕСКИЙ): _finish_op() умирает молча на не-словаре
|
|
||||||
|
|
||||||
**Файл:** `site/routes/api_test.py`, строки 176-179
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _finish_op(...):
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
data = client.get(...) # ← except ловит ТОЛЬКО это
|
|
||||||
except Exception:
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
op = data.get("instanceOperation", {}) # ← если data не dict → AttributeError!
|
|
||||||
```
|
|
||||||
|
|
||||||
Если Nubes API возвращает `list`, `str`, `None` или любой не-словарь — `data.get()` → **AttributeError**. Этот except НЕ покрывает строку 179. Поток умирает молча. Python daemon-потоки не пишут traceback.
|
|
||||||
|
|
||||||
**Решение:** обернуть ВСЁ тело цикла (строки 166-195) в `try/except Exception: print(traceback)`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟡 Баг #3 (НЕКРИТИЧНЫЙ): _op_results — та же multi-worker проблема
|
|
||||||
|
|
||||||
**Файл:** `site/routes/api_test.py`, строка 152
|
|
||||||
|
|
||||||
```python
|
|
||||||
_op_results = {} # module-level
|
|
||||||
|
|
||||||
# В _finish_op (воркер A):
|
|
||||||
_op_results[op_uid] = {"status": "OK", ...}
|
|
||||||
|
|
||||||
# В api_test_status (воркер B):
|
|
||||||
if op_uid in _op_results: # ← False! (другой воркер)
|
|
||||||
return jsonify(_op_results[op_uid])
|
|
||||||
# fallback → прямой запрос в Nubes API
|
|
||||||
```
|
|
||||||
|
|
||||||
**Некритично** потому что есть fallback: если `_op_results` не содержит op_uid, `api_test_status()` делает прямой GET в Nubes API и возвращает статус. UI получает stages напрямую из Nubes, не из `_op_results`.
|
|
||||||
|
|
||||||
**НО:** fallback не обновляет `tracker_remove` для delete. При multi-worker delete не удалит инстанс из трекера.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Полная трассировка CREATE (все шаги)
|
|
||||||
|
|
||||||
| Шаг | Код | Результат | Статус |
|
|
||||||
|-----|-----|-----------|--------|
|
|
||||||
| 1 | `data = request.get_json()` | `svc_id=1, op_name="create", display_name="autotest-1-xxx"` | ✅ |
|
|
||||||
| 2 | `client.post("/instances", ...)` | `instance_uid = "f192b10d-..."` | ✅ |
|
|
||||||
| 3 | `client.post("/instanceOperations", ...)` | `op_uid = "d489348e-..."` | ✅ |
|
|
||||||
| 4 | `for pid,pval: client.post("/instanceOperationCfsParams", ...)` | Параметры установлены | ✅ |
|
|
||||||
| 5 | `client.post("/instanceOperations/{op_uid}/run")` | Операция запущена | ✅ |
|
|
||||||
| 6 | `tracker_add(instance_uid, svc_id, display_name)` | `_data[uid] = {...}` (in-memory) | ✅ |
|
|
||||||
| 7 | `threading.Thread(target=_finish_op, ...)` | Поток запущен | ✅ |
|
|
||||||
| 8 | `return {status:"RUNNING", opUid, instanceUid}` | Ответ UI | ✅ |
|
|
||||||
| 9 | UI poll `/api/test/status/<opUid>` | `_op_results[opUid]` или fallback API | ⚠️ разн. воркеры |
|
|
||||||
| 10 | `_finish_op` детектит `dtFinish` | `_op_results[opUid] = {status:"OK"}` | ⚠️ воркер A |
|
|
||||||
| 11 | UI: `refreshInstances()` → `/api/operations/1` | `tracker_list()` → 4 элемента | ❌ воркер B |
|
|
||||||
| 12 | `api_operations()` возвращает 4 инстанса | Нового нет | ❌ |
|
|
||||||
| 13 | F5 → `selectService(1)` → `/api/operations/1` | Опять 4 | ❌ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Дополнительные находки (не критические)
|
|
||||||
|
|
||||||
### ⚠️ params loop — orphaned resources при ошибке
|
|
||||||
Если `client.post("/instanceOperationCfsParams", ...)` падает на mid-param:
|
|
||||||
- Инстанс УЖЕ создан в Nubes (сирота)
|
|
||||||
- Операция УЖЕ создана (сирота)
|
|
||||||
- `tracker_add` НЕ вызван (он после цикла)
|
|
||||||
- Ответ: FAIL
|
|
||||||
|
|
||||||
### ⚠️ `data["svcOperationId"]` — KeyError если поле отсутствует
|
|
||||||
Вызов API без `svcOperationId` в JSON → KeyError → FAIL. Обработано внешним try/except.
|
|
||||||
|
|
||||||
### ⚠️ `int(pid)` — ValueError если param ID не число
|
|
||||||
`int("abc")` → ValueError → FAIL. Обработано внешним try/except.
|
|
||||||
|
|
||||||
### ⚠️ `_find_uid()` итерация по ВСЕМ значениям
|
|
||||||
Может случайно найти uid во вложенном объекте. Низкий риск, но нечисто.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## План исправлений для v1.0.51
|
|
||||||
|
|
||||||
| # | Что | Как |
|
|
||||||
|---|-----|-----|
|
|
||||||
| 1 | Файловый трекер с fcntl.flock | Вернуть `/tmp/instances.json`, заменить `threading.Lock` на `fcntl.flock` |
|
|
||||||
| 2 | _finish_op: try/except на всё тело | Обернуть строки 166-195 в `try/except: print(traceback)` |
|
|
||||||
| 3 | (опционально) `svc_id = int(data["serviceId"])` | Защита от строкового "1" в JSON |
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# 2026-07-30 — Сессия (v1.1.51 → v1.1.55)
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
Сценарий `dummy_test` падает на шаге CREATE: `no instanceUid in response`. Ручное создание Болванки через UI работает.
|
|
||||||
|
|
||||||
## Найденные и исправленные баги
|
|
||||||
|
|
||||||
### Баг #1 — no instanceUid in response (ЛОЖНАЯ ТРЕВОГА)
|
|
||||||
**Где:** `scenario.py:122` — `resp.get("instanceUid")` возвращал None.
|
|
||||||
|
|
||||||
**Причина:** сценарий НЕ перезапускался после деплоя v1.1.52. UI показывал старый результат из БД от v1.1.51 (баг #3).
|
|
||||||
|
|
||||||
**Проверка curl:** API отвечает корректно — 201, `Location: ./UUID`, тело `""`. `http_client.py` правильно парсит.
|
|
||||||
|
|
||||||
**Добавлено в v1.1.52:** `_status` (HTTP-код) в результат `http_client.post()` для отладки.
|
|
||||||
|
|
||||||
### Баг #2 — лишний svcOperationId для create (ИСПРАВЛЕН в v1.1.52)
|
|
||||||
**Где:** `scenario.py:127`
|
|
||||||
|
|
||||||
Для `create` операция `/instanceOperations` не должна содержать `svcOperationId` (по спецификации Terraform). Было одинаково для всех операций, стало:
|
|
||||||
```python
|
|
||||||
if op_name == "create":
|
|
||||||
op_payload = {"instanceUid": instance_uid, "operation": op_name}
|
|
||||||
else:
|
|
||||||
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Баг #3 — старые результаты сценариев после редеплоя (ИСПРАВЛЕН в v1.1.53)
|
|
||||||
**Где:** `api_scenario.py` + `app.js`
|
|
||||||
|
|
||||||
**Симптом:** после редеплоя UI показывал результат сценария от СТАРОЙ версии (из БД), пользователь думал что новый код не работает.
|
|
||||||
|
|
||||||
**Причина:** `/api/scenario/status` не возвращал `app_version`, фронтенд не фильтровал по версии.
|
|
||||||
|
|
||||||
**Исправление:**
|
|
||||||
- `api_scenario.py`: добавлен `app_version` в SELECT обоих эндпоинтов
|
|
||||||
- `app.js`: `loadScenarios()` фильтрует историю по `window.APP.version` — показывает только запуски текущей версии
|
|
||||||
|
|
||||||
### Баг #4 — нельзя удалять без suspend (ОБНАРУЖЕН)
|
|
||||||
DELETE после CREATE падает: «Невозможно выполнить операцию удаления услуги. Услуга не остановлена».
|
|
||||||
Нужно сначала suspend + 15 мин ожидания. **Вывод:** не использовать delete в сценариях.
|
|
||||||
|
|
||||||
## Хронология
|
|
||||||
|
|
||||||
### v1.1.52 — fix scenario create + http_client _status debug
|
|
||||||
- `scenario.py`: убран `svcOperationId` из create payload
|
|
||||||
- `http_client.py`: добавлен `{"_status": r.status_code}` в результат для отладки
|
|
||||||
|
|
||||||
### v1.1.53 — filter scenario history by app_version
|
|
||||||
- `api_scenario.py`: `app_version` в SELECT
|
|
||||||
- `app.js`: фильтр `verHistory = srHistory.filter(r => r.app_version === curVer)`
|
|
||||||
|
|
||||||
### v1.1.55 — split large files + CRUD scenario editor
|
|
||||||
|
|
||||||
**Backend (5→7 файлов):**
|
|
||||||
- `api_test.py`: 622→479 строк, terraform-функции → `operations/terraform.py` (142)
|
|
||||||
- `api_scenario.py` → `api_scenario_run.py` (153) + `api_scenario_defs.py` (108)
|
|
||||||
- `db/scenario_defs.py`: + `client_id`, `stand`, `is_seed` в `list_definitions`
|
|
||||||
- `api_scenario_defs.py`: `_validate_steps()` — валидация шагов перед create/update
|
|
||||||
- `scenario.py`: импорт `send_params_terraform` из `operations/terraform.py` (вместо кросс-импорта из routes)
|
|
||||||
|
|
||||||
**Frontend (1→10 файлов):**
|
|
||||||
- `app.js`: 554→16 (загрузчик)
|
|
||||||
- `utils.js` (45), `instances.js` (89), `operations.js` (249)
|
|
||||||
- `history.js` (35), `scenario-list.js` (90)
|
|
||||||
- CRUD: `scenario-form.js` (128), `scenario-create.js` (25, +clone), `scenario-edit.js` (12), `scenario-delete.js` (13)
|
|
||||||
- Кнопки [▶][✏][⎘][🗑], seed-сценарии только [▶][⎘]
|
|
||||||
- Inline-редактор: имя, шаги (service_id, operation, params), 409 conflict
|
|
||||||
|
|
||||||
**Максимальный размер файла:** JS 249 строк, Python 479 строк.
|
|
||||||
|
|
||||||
## Agent consultations
|
|
||||||
|
|
||||||
### Вопрос к Sonnet: план CRUD-редактирования сценариев
|
|
||||||
|
|
||||||
**Суть:** сейчас сценарии правятся только через `scenario_seed.yaml` → редеплой. Нужно редактирование через UI без редеплоя.
|
|
||||||
|
|
||||||
**Ответ Sonnet (ключевые выводы):**
|
|
||||||
|
|
||||||
1. **CRUD-эндпоинты УЖЕ реализованы** в `api_scenario.py` (я ошибался, они есть):
|
|
||||||
- `GET/POST /api/scenario/definitions`
|
|
||||||
- `GET/PUT/DELETE /api/scenario/definitions/<id>`
|
|
||||||
- DB-функции в `scenario_defs.py` полностью готовы
|
|
||||||
|
|
||||||
2. **Нужны только 3 мелкие правки бэкенда:**
|
|
||||||
- `list_definitions`: добавить `client_id`, `stand`, `is_seed` в SELECT
|
|
||||||
- `api_scenario.py`: `_validate_steps()` с проверкой service_id, operation, params
|
|
||||||
- `index.html`: `window.APP.services` (список сервисов для фронтенда)
|
|
||||||
|
|
||||||
3. **Фронтенд — ~200 строк JS:**
|
|
||||||
- Кнопки [▶][✏][⎘][🗑] у каждого сценария
|
|
||||||
- Inline-редактор: имя, шаги (сервис▾, операция▾, параметры)
|
|
||||||
- Автоподгрузка операций при смене сервиса (`GET /api/operations/{svcId}`)
|
|
||||||
- Автозаполнение параметров при смене операции (`GET /api/params/{svcOpId}`)
|
|
||||||
- Оптимистичная блокировка (version → 409)
|
|
||||||
|
|
||||||
4. **Безопасность:** изоляция по client_id+stand, seed-сценарии только [▶][⎘]
|
|
||||||
|
|
||||||
## Отладка через kubectl
|
|
||||||
- SSH: `naeel@5.172.178.213` (ключ `secrets/id_ed25519.txt`)
|
|
||||||
- Неймспейс autotest: `01a2d5b2-4df4-4cfe-b98e-8dd3534a3bb5`
|
|
||||||
- Код в поде: `/var/www/site/`
|
|
||||||
- Проверка версии: `kubectl exec -n $NS deploy/pythonk8s -c app -- grep VERSION /var/www/site/app.py`
|
|
||||||
@@ -1,805 +0,0 @@
|
|||||||
# 2026-07-31 — Сессия (v1.1.57 → v1.2.1)
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
Обсуждение архитектуры: унификация ручного и сценарного режимов, гибкие ссылки на инстансы, новый UI редактора сценариев.
|
|
||||||
|
|
||||||
## Agent consultations
|
|
||||||
|
|
||||||
### Промпт для Opus
|
|
||||||
Составлен `DOCS/opus-architecture-prompt.md` — полное описание проекта, дублирование CREATE-флоу, ограничение instance_map, вопросы по архитектуре.
|
|
||||||
|
|
||||||
### Ответ Opus — 19 уточняющих вопросов
|
|
||||||
|
|
||||||
**A. Единый executor (operations/executor.py)**
|
|
||||||
1. Граница ответственности: executor делает всё до /run, поллинг — снаружи?
|
|
||||||
2. Обработка ошибок: исключения с типом или dict {ok, error, failed_step}?
|
|
||||||
3. Трекер: вызывать tracker_add внутри executor для create? Нужны ли сценарные инстансы в трекере?
|
|
||||||
4. _finish_op: остаётся в api_test.py или выносим в общий модуль поллинга?
|
|
||||||
|
|
||||||
**B. Гибкие ссылки на инстансы**
|
|
||||||
5. Приоритет резолвинга: instance_uid > instance_ref > (для create — новый)
|
|
||||||
6. Где хранить output→uid: в памяти (instance_map) или в scenario_runs.instance_bindings?
|
|
||||||
7. Валидация _validate_steps: проверять ссылки на output, уникальность, обязательность
|
|
||||||
8. Обратная совместимость: поддерживать старый формат (service_id) или мигрировать?
|
|
||||||
|
|
||||||
**C. Схема БД**
|
|
||||||
9. Формат в steps JSONB без изменений схемы? (подтверждено)
|
|
||||||
10. Использовать существующую колонку scenario_runs.instance_bindings?
|
|
||||||
|
|
||||||
**D. UI редактора**
|
|
||||||
11. Модал vs inline — рекомендация: модал на весь экран
|
|
||||||
12. Переиспользовать renderParamRow из operations.js в общем модуле?
|
|
||||||
13. Pre-fill параметров: авто или по кнопке?
|
|
||||||
14. Формат параметров: символические имена в БД (как сейчас) или иное?
|
|
||||||
|
|
||||||
**E. Оптимизация**
|
|
||||||
15. Вынести _find_uid/_uid_from_location в общий utils
|
|
||||||
16. Механизм восстановления зависших scenario_runs?
|
|
||||||
17. Ограничение lock_check: только один RUNNING сценарий — осознанное?
|
|
||||||
18. _op_results в памяти → мигрировать в БД/Redis?
|
|
||||||
19. Приоритет: бэкенд или UI редактора в первую очередь?
|
|
||||||
|
|
||||||
## Ключевые решения (отвечено DeepSeek)
|
|
||||||
|
|
||||||
**A. Executor:**
|
|
||||||
- A1. Граница: до /run включительно, без поллинга. Возвращает {instance_uid, op_uid, display_name}
|
|
||||||
- A2. Ошибки: dict {ok, error, failed_step}, не исключения
|
|
||||||
- A3. Трекер: да, tracker_add внутри executor для всех create
|
|
||||||
- A4. _finish_op: оставить в api_test.py, цикл поллинга → operations/poll.py
|
|
||||||
|
|
||||||
**B. Ссылки:**
|
|
||||||
- B5. Приоритет: instance_uid > instance_ref > новый create
|
|
||||||
- B6. Хранение: память + scenario_runs.instance_bindings
|
|
||||||
- B7. Валидация: проверять ссылки, уникальность, обязательность
|
|
||||||
- B8. Совместимость: оба формата, без миграции, fallback на service_id
|
|
||||||
|
|
||||||
**C. БД:**
|
|
||||||
- C9. Без изменений схемы
|
|
||||||
- C10. Использовать instance_bindings
|
|
||||||
|
|
||||||
**D. UI:**
|
|
||||||
- D11. Модал на весь экран
|
|
||||||
- D12. Общий модуль params-render.js
|
|
||||||
- D13. Авто pre-fill, все параметры с defaults
|
|
||||||
- D14. Символические имена в БД
|
|
||||||
|
|
||||||
**E. Оптимизация:**
|
|
||||||
- E15. _find_uid → api/utils.py
|
|
||||||
- E16. Startup check: TIMEOUT для зависших >1ч
|
|
||||||
- E17. lock_check оставить
|
|
||||||
- E18. _op_results не в scope
|
|
||||||
- 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)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# 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) |
|
|
||||||
|
|
||||||
## Формат
|
|
||||||
|
|
||||||
Каждая сессия содержит:
|
|
||||||
- Контекст (что обсуждалось)
|
|
||||||
- Принятые решения
|
|
||||||
- Реализованные изменения (версии)
|
|
||||||
- Ошибки и как исправлены
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
есть облачный ЛЛМ 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; фронт вызывает по кнопке «Разобрать ошибку»/«Сгенерировать». **Ничего не меняю — это ответ; скажешь «делай», добавлю в план как отдельную фазу.**
|
|
||||||
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
Список задач обновлен
|
|
||||||
|
|
||||||
# Что получится из Autotest
|
|
||||||
|
|
||||||
**Autotest** станет веб-приложением для автоматической проверки операций облачных сервисов Nubes.
|
|
||||||
|
|
||||||
Сейчас приложение позволяет вручную создавать, изменять и удалять инстансы. Следующий этап — запуск готовых автоматических сценариев, например:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Создать PostgreSQL → изменить параметры → проверить результат → удалить
|
|
||||||
```
|
|
||||||
|
|
||||||
## Как это будет работать
|
|
||||||
|
|
||||||
1. В конфигурационном YAML-файле задаётся:
|
|
||||||
- какие сервисы проверять;
|
|
||||||
- какие операции выполнять;
|
|
||||||
- в какой последовательности;
|
|
||||||
- какие параметры использовать;
|
|
||||||
- нужно ли удалять созданный инстанс.
|
|
||||||
|
|
||||||
2. Пользователь открывает приложение, выбирает сценарий и стенд, нажимает **«Запустить»**.
|
|
||||||
|
|
||||||
3. Приложение последовательно выполняет операции и показывает:
|
|
||||||
- текущий шаг;
|
|
||||||
- созданный инстанс;
|
|
||||||
- продолжительность;
|
|
||||||
- успешный или неуспешный результат;
|
|
||||||
- причину ошибки.
|
|
||||||
|
|
||||||
4. Результаты сохраняются в PostgreSQL и доступны всем тестировщикам в общей истории.
|
|
||||||
|
|
||||||
## Архитектура
|
|
||||||
|
|
||||||
```text
|
|
||||||
YAML-сценарии в Git
|
|
||||||
↓
|
|
||||||
Flask-приложение
|
|
||||||
↓
|
|
||||||
Исполнитель сценариев
|
|
||||||
↓
|
|
||||||
Nubes API
|
|
||||||
↓
|
|
||||||
PostgreSQL: запуски, шаги, результаты и ошибки
|
|
||||||
```
|
|
||||||
|
|
||||||
Ручные операции и автоматические сценарии будут использовать один и тот же механизм работы с Nubes API. Благодаря этому автоматический тест будет выполнять операцию так же, как текущий ручной интерфейс и UI облака.
|
|
||||||
|
|
||||||
Для каждого стенда используется отдельный технический токен. Токен пользователя нужен только для ручных операций.
|
|
||||||
|
|
||||||
## Первый рабочий релиз
|
|
||||||
|
|
||||||
В MVP войдут:
|
|
||||||
|
|
||||||
- YAML-сценарии, хранящиеся и проверяемые через Git;
|
|
||||||
- ручной запуск сценария из веб-интерфейса;
|
|
||||||
- последовательное выполнение операций;
|
|
||||||
- поддержка dev и test;
|
|
||||||
- сценарии с временными и постоянными тестовыми инстансами;
|
|
||||||
- отображение выполнения по шагам;
|
|
||||||
- общая история всех запусков;
|
|
||||||
- сохранение результатов и ошибок в PostgreSQL;
|
|
||||||
- защита от одновременного запуска нескольких сценариев на одном стенде.
|
|
||||||
|
|
||||||
Prod на первом этапе будет заблокирован. После проверки на dev и test добавим отдельные правила безопасности для тестовой организации, карантина и разрешённых операций.
|
|
||||||
|
|
||||||
## Дальнейшее развитие
|
|
||||||
|
|
||||||
После MVP можно добавить:
|
|
||||||
|
|
||||||
- запуск по расписанию;
|
|
||||||
- уведомления о падениях;
|
|
||||||
- регулярные проверки выбранных сервисов;
|
|
||||||
- статистику успешности и длительности операций;
|
|
||||||
- безопасный запуск на prod;
|
|
||||||
- настройку сценариев через веб-интерфейс;
|
|
||||||
- impersonation и проверку операций от разных пользователей.
|
|
||||||
|
|
||||||
**Итог:** это будет не копия UI облака, а централизованный инструмент для регулярной проверки работоспособности операций Nubes: с настраиваемыми сценариями, общими результатами и контролем безопасности.
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
# Задача: архитектура эмулятора 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
|
|
||||||
- Без БД, всё в памяти
|
|
||||||
- Без многопоточности (один процесс, один пользователь)
|
|
||||||
- Код должен быть ПРОСТЫМ — эмулятор не должен быть сложнее тестируемого приложения
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# Задача: универсальный генератор моков из 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-файлов
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# 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` |
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# Гайд: когда и как использовать 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 строк) |
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
# План: 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 → работает
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# Архитектурный аудит 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) — отложить
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Моё мнение — сравнение аудитов Опуса и 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 → Опус. Сначала закрыть риски эксплуатации, потом архитектурные улучшения.
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# Архитектурный аудит 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? Что критично доделать?
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# Архитектурный аудит 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. Косметика версии
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
# 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'ами?
|
|
||||||
|
|
||||||
## Формат ответа
|
|
||||||
|
|
||||||
Сгруппируй находки:
|
|
||||||
- 🔴 Критические (сломает работу)
|
|
||||||
- 🟡 Средние (потенциальная проблема)
|
|
||||||
- 🔵 Минорные (стиль, имена)
|
|
||||||
|
|
||||||
Для каждой: файл, проблема, предлагаемое исправление.
|
|
||||||
|
|
||||||
Если по какой-то категории всё ок — напиши «проблем не найдено».
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
# Задача: 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: <краткий заголовок>
|
|
||||||
<развёрнутый вопрос>
|
|
||||||
```
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
# Соннет: анализ сравнительного тестирования 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 — достаточно. Разработчик запускает вручную, смотрит
|
|
||||||
глазами. Файл отчёта переусложнит. Если понадобится история — можно потом.
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
# Консультация: интеграция 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 НЕ задан явно?
|
|
||||||
|
|
||||||
Какой подход надёжнее?
|
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
# Задача: спроектировать мок-полигон 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: <краткий заголовок>
|
|
||||||
<развёрнутый вопрос с контекстом>
|
|
||||||
```
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
# Ответ 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()`.
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Задача: улучшить 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 главных улучшений которые дадут максимальный эффект при минимуме правок.
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
# Результаты тестирования 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
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# Ссылки: генератор 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