765 lines
39 KiB
Markdown
765 lines
39 KiB
Markdown
# 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`).
|