Files
autotest/HISTORY/2026-07-31-session.md
T

714 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()` для всех строк (`&gt;``>`, `&quot;``"`)
- Рекурсивный `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.