# 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: ./` на 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 ` — без хардкода, читает все .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` | операции сервиса | | 5 | GET | `/api/v1/svc/instances` | пагинация (pageSize, page) | | 6 | GET | `/api/v1/svc/instances/` | полный instance+state | | 7 | POST | `/api/v1/svc/instances` | 201 + Location: ./{uid} | | 8 | GET | `/api/v1/svc/instanceOperations/default/` | cfsParams с dataDescriptor | | 9 | POST | `/api/v1/svc/instanceOperations` | 201 + Location, auto-find create opId | | 10 | GET | `/api/v1/svc/instanceOperations/` | +cfsParams если ?fields=... | | 11 | POST | `/api/v1/svc/instanceOperationCfsParams` | paramId → value | | 12 | GET | `/api/v1/svc/instanceOperations//validate-cfs` | **пустое тело**, 200 | | 13 | POST | `/api/v1/svc/instanceOperations//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/` | изменить MOCK_OP_DELAY | Критические детали: - `default/` строго ДО `` в маршрутах 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.