diff --git a/.gitignore b/.gitignore index 05fc926..6f9bbf7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ secrets/* # Submodules / nested repos app-autotest/ +polygon/ # OS files .DS_Store @@ -19,6 +20,7 @@ Thumbs.db # IDE .idea/ .vscode/ +.venv/ *.iml # Go (embed.go generates from yaml) diff --git a/DOCS/README.md b/DOCS/README.md new file mode 100644 index 0000000..3fa0b18 --- /dev/null +++ b/DOCS/README.md @@ -0,0 +1,103 @@ +# DOCS — Архив документации + +## Архитектура + +| Файл | Описание | +|------|----------| +| `ARCHITECTURE.md` | Актуальная архитектура: эндпоинты, схема БД, безопасность (§8 с фиксами аудита) | +| `ARCHITECTURE-FULL.md` | Развёрнутая архитектура: полный список файлов, все роуты, зависимости | +| `ARCHITECTURE.legacy.md` | Архитектура v1.0.x (до рефакторинга) | +| `architecture-final.md` | Финальная архитектура после всех рефакторингов и аудитов | +| `architecture-next.md` | Черновик архитектуры vNext — идеи на будущее | +| `architecture-questions-for-sonnet.md` | Вопросы к Sonnet по архитектуре (перед ревью) | +| `architecture-review-sonnet.md` | Ревью архитектуры от Sonnet | +| `architecture-round2.md` | Второй раунд архитектурных решений | + +## Как делать (How-to) + +| Файл | Описание | +|------|----------| +| `howto-flask-nubes.md` | **ВАЖНО.** Как писать Flask-приложение под Nubes: структура `site/`, порт 5000, `app.run()`, запреты | + +## API Nubes + +| Файл | Описание | +|------|----------| +| `api-access.md` | Как получить доступ к Nubes API: токены, clientId, стенды | +| `api-create-flow.md` | Полный flow создания инстанса: instances → instanceOperations → params → run → poll | +| `api-operation-stages.md` | Стадии выполнения операций (plan, apply, etc.) | +| `terraform-operations-full-logic.md` | Логика Terraform-операций: validate-cfs, refSvc, нормализация параметров | + +## Планы + +| Файл | Описание | +|------|----------| +| `polygon-plan.md` | **ВАЖНО.** План мок-полигона: 3 фазы, 12 шагов, YAML-спецификация | +| `opus-plan-2026-07-31.md` | План Опуса: унификация executor, гибкие ссылки, 4 фазы 13 шагов | +| `test-results-history-plan.md` | План истории результатов тестов | +| `gpt56-sol-plan.md` | План Sol по GPT-5.6 | + +## Промпты для агентов + +| Файл | Описание | +|------|----------| +| `agent-analysis-request.md` | Запрос на анализ проекта (архитектура, риски, рекомендации) | +| `opus-architecture-prompt.md` | Промпт Опусу: полное описание проекта + вопросы по архитектуре | +| `opus-frontend-prompt.md` | Промпт Опусу: вопросы по фронтенду (instance dropdown) | +| `gpt56-sol-prompt.md` | Промпт GPT-5.6: описание проекта для Sol | +| `sonnet-review-prompt.md` | Промпт Sonnet для code review | + +## Ревью и аудиты — Sonnet + +| Файл | Описание | +|------|----------| +| `sonnet-full-code-review.md` | Полный code review всех файлов | +| `sonnet-full-response.md` | Полный ответ Sonnet на review | +| `sonnet-full-review.md` | Сводка review | +| `sonnet-architecture-review-v1.0.89.md` | Архитектурное ревью v1.0.89 | +| `sonnet-architecture-review-v1.0.89-r2.md` | Второй раунд архитектурного ревью | +| `sonnet-review-v1.1.11.md` | Ревью v1.1.11 | +| `sonnet-review-answers.md` | Ответы на замечания Sonnet | +| `sonnet-review-round2.md` | Второй раунд ревью | +| `sonnet-review-round3.md` | Третий раунд ревью | +| `sonnet-final-audit.md` | Финальный аудит | +| `sonnet-final-review.md` | Финальное ревью | +| `sonnet-missed-bugs.md` | Пропущенные баги (что Sonnet не нашёл) | +| `sonnet-params-and-audit.md` | Параметры + аудит | +| `sonnet-response-params-audit.md` | Ответ: параметры и аудит | +| `sonnet-response-final.md` | Финальный ответ | +| `sonnet-response-round2.md` | Ответ второго раунда | +| `sonnet-response-v1.1.11.md` | Ответ на ревью v1.1.11 | +| `sonnet-response-serviceInstanceUid.md` | Ответ про serviceInstanceUid | +| `sonnet-response-state-out.md` | Ответ про state.out | +| `sonnet-state-out-valuelist.md` | stateOut + valueList | +| `sonnet-terraform-diff.md` | Разбор Terraform diff | +| `sonnet-new-chat.md` | Новый чат с Sonnet | +| `sonnet-question-tracker.md` | Трекер вопросов к Sonnet | + +## Ревью — Опус + +| Файл | Описание | +|------|----------| +| `opus-questions-2026-07-31.md` | 19 уточняющих вопросов Опуса по архитектуре | +| `opus-instance-dropdown-questions.md` | Вопросы Опуса про instance dropdown | +| `opus-review-v1.2.0.md` | Ревью v1.2.0 от Опуса | + +## Вопросы-ответы + +| Файл | Описание | +|------|----------| +| `questions-to-sol.md` | Вопросы к Sol | +| `sol-answers.md` | Ответы Sol | +| `sol-scenario-editor.md` | Sol про редактор сценариев | + +## Прочее + +| Файл | Описание | +|------|----------| +| `HISTORY.md` | Краткая хронология версий (v1.0.x → v1.2.x) | +| `dialogues.md` | Диалоги/дискуссии в процессе разработки | +| `service-categories.md` | Категории сервисов Nubes (37 сервисов, их типы) | +| `review-comparison-sonnet-opus.md` | Сравнение ревью Sonnet vs Opus | +| `step-by-step-audit-v1.0.50.md` | Пошаговый аудит v1.0.50 | +| `vm-213.md` | Заметки о VM-213 (тестовый стенд) | diff --git a/HISTORY/2026-07-31-session.md b/HISTORY/2026-07-31-session.md index 002797f..af80166 100644 --- a/HISTORY/2026-07-31-session.md +++ b/HISTORY/2026-07-31-session.md @@ -504,3 +504,50 @@ convert_all(stands_dir) → dict[int, config] Конвертер помечает такие операции: `{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.` diff --git a/HISTORY/README.md b/HISTORY/README.md new file mode 100644 index 0000000..7200631 --- /dev/null +++ b/HISTORY/README.md @@ -0,0 +1,27 @@ +# HISTORY — Хронология сессий + +| Файл | Дата | Содержание | +|------|------|------------| +| `2026-07-23-session-1.md` | 23.07.2026 | Сессия 1 | +| `2026-07-23-session-2.md` | 23.07.2026 | Сессия 2 | +| `2026-07-23-session-3.md` | 23.07.2026 | Сессия 3 | +| `2026-07-23-session-4.md` | 23.07.2026 | Сессия 4 | +| `2026-07-24-all-failures.md` | 24.07.2026 | Все ошибки/падения | +| `2026-07-24-har-analysis.md` | 24.07.2026 | Анализ HAR-файлов | +| `2026-07-24-session-1.md` | 24.07.2026 | Сессия 1 | +| `2026-07-24-session-2.md` | 24.07.2026 | Сессия 2 | +| `2026-07-24-session-3.md` | 24.07.2026 | Сессия 3 | +| `2026-07-24-session-4.md` | 24.07.2026 | Сессия 4 | +| `2026-07-24-session-5.md` | 24.07.2026 | Сессия 5 | +| `2026-07-24-session-6.md` | 24.07.2026 | Сессия 6 | +| `2026-07-29-session.md` | 29.07.2026 | Сессия: CMDB delete, переписка с Георгием | +| `2026-07-30-session.md` | 30.07.2026 | Сессия | +| `2026-07-31-session.md` | 31.07.2026 | **Текущая.** Аудит Codex (3 раунда), полигон (план + отдельная репа), v1.2.16→v1.2.23 | + +## Формат + +Каждая сессия содержит: +- Контекст (что обсуждалось) +- Принятые решения +- Реализованные изменения (версии) +- Ошибки и как исправлены diff --git a/polygon-docs/sonnet-polygon-prompt.md b/polygon-docs/sonnet-polygon-prompt.md new file mode 100644 index 0000000..b6e2980 --- /dev/null +++ b/polygon-docs/sonnet-polygon-prompt.md @@ -0,0 +1,339 @@ +# Задача: спроектировать мок-полигон Nubes API + +> Адресат: Claude Sonnet 4.6 +> Дата: 2026-07-31 +> ⛔ ОТВЕТ — ТОЛЬКО В ЧАТ. Не редактировать файлы. Не создавать файлы. Только текст в чат. + +--- + +## 1. Что такое polygon + +**Polygon** — отдельный managed-сервис на Nubes (`polygon.pythonk8s.dev.nubes.ru`), +эмулирующий REST API облачной платформы Nubes. Нужен для интеграционных тестов +приложения **app-autotest** — чтобы тесты гонялись не на реальном облаке, а на +локальном/CI эмуляторе. + +**Ключевое:** polygon — это НЕ часть app-autotest. Это самостоятельный сервис +со своим репозиторием (`https://gitea.services.ngcloud.ru/forcloud/polygon.git`), +своим деплоем, своей версией (v0.1.0). + +### Текущее состояние + +Сервис запущен на Nubes, код минимальный: + +``` +polygon/ +├── requirements.txt # Flask>=3.0, gunicorn>=21.2, PyYAML>=6.0 +├── README.md +├── .gitignore +└── site/ + └── app.py # 4 эндпоинта: /health, /, /api/v1/svc/instances, + # /api/v1/svc/_mock/reset +``` + +`site/app.py` — стандартный Flask по инструкции Nubes: +- `app = Flask(__name__, template_folder="templates", static_folder="static")` +- `/health` → `"OK"` (healthcheck) +- `/` → HTML-страница с версией +- `app.run(debug=True, host="0.0.0.0", port=5000)` в `if __name__ == "__main__"` + +Gunicorn на проде: `gunicorn app:app --workers 2 --bind 0.0.0.0:8000`. + +## 2. Исходные данные: STANDS YAML + +В `STANDS/test/resources_yaml/` лежат **37 YAML-файлов** — описания ВСЕХ сервисов +Nubes в формате Terraform-провайдера. Это КАНОНИЧЕСКИЙ источник данных о сервисах: +их параметры, операции, типы данных, дефолты, valueList'ы. + +### Структура STANDS YAML (на примере dummy и postgres) + +```yaml +# 1_dummy.yaml (простой сервис, 339 строк) +name: dummy +service_id: 1 +service_display_name: Болванка +service_short_name: dummy +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params # map + - code: state_out # map + - code: vault_secrets # map, sensitive + # ... ещё 5 outputs +operations: + - name: create + id: 18 + kind: instance + action: create + params: + - id: 242 + code: resourceRealm + data_type: string + required: true + default: dummy + value_list: [dummy] + - id: 198 + code: durationMs + data_type: "integer >= 0" + required: true + default: "0" + - id: 199 + code: failAtStart + data_type: boolean + required: true + default: "false" + value_list: ["false", "true"] + - id: 321 + code: someMapParam + data_type: map + sub_params: + - id: 322 + code: subparam1 + data_type: string + - id: 323 + code: secret + data_type: string + - name: modify + id: 92 + kind: instance + action: modify + params: [...] # те же параметры, is_modifiable: true + - name: delete + id: 71 + ... +``` + +```yaml +# 90_postgres.yaml (самый сложный, 934 строки) +operations: + - name: create # id: 19, kind: instance + - name: delete # id: 20 + - name: modify # id: 40 + - name: suspend # id: 41 + - name: resume # id: 42 + - name: restart # id: 43 ← лишняя? + - name: recovery # id: 24 ← лишняя? + - name: create_user # id: 44 ← subresource! + - name: delete_user # id: 45 ← subresource! + - name: create_database # id: 46 ← subresource! + - name: delete_database # id: 47 ← subresource! + + # У create-операции — map-fixed с sub_params: + - name: create + params: + - id: 788 + code: clusterConfiguration + data_type: map-fixed + sub_params: + - id: 106 + code: replicas + data_type: "integer > 0" + default: "1" + value_list: ["1", "3", "5", "7"] + - id: 107 + code: disk + data_type: "integer > 0" + default: "10" +``` + +### Спектр сложности (37 сервисов) + +| Тип | Примеры | Особенности | +|-----|---------|-------------| +| Простые | dummy, s3, http, gitea, nodejs, flask | Плоские параметры, 6 операций | +| Средние | redis, mongodb, rabbitmq, clickhouse, kafka | map-fixed (cluster/access/startup) | +| Сложные | postgres, mariadb | + subresources (create_user, create_database) | +| Инфраструктурные | vc_org, vc_vdc, vc_nsxt, vapp, vc_vm_v2/v3 | Другие kind'ы, refSvcId на другие сервисы | +| Особые | s3bucket (refSvcId на s3), template, k8s_velero, llm_ai | Нестандартные параметры | + +Все 37 файлов лежат здесь: `STANDS/test/resources_yaml/` +Полный список: `1_dummy.yaml`, `2_template.yaml`, `12_s3.yaml`, `13_s3bucket.yaml`, +`19_vc_org.yaml`, `21_vc_vdc.yaml`, `22_vc_nsxt.yaml`, `25_vcexternalip.yaml`, +`26_vapp.yaml`, `27_vc_vm_v2.yaml`, `28_vc_vm_v3.yaml`, `29_vc_vdc_group.yaml`, +`50_nextcloud.yaml`, `81_superset.yaml`, `82_harbor.yaml`, `86_k8s_velero.yaml`, +`89_flask.yaml`, `90_postgres.yaml`, `91_redis.yaml`, `92_mongodb.yaml`, +`93_rabbitmq.yaml`, `94_lucee.yaml`, `95_nodejs.yaml`, `96_pgadmin.yaml`, +`98_http.yaml`, `99_gitea.yaml`, `109_zones_v2.yaml`, `111_dnsrecord.yaml`, +`115_mariadb.yaml`, `116_kafka.yaml`, `119_akhq.yaml`, `120_clickhouse.yaml`, +`148_vc_mgmt_sthutrval_cluster.yaml`, `149_valo_tenant.yaml`, +`150_k8s_sthutrval_cluster.yaml`, `151_k8s_openbao.yaml`, `163_llm_ai.yaml`. + +## 3. Архитектурные решения (уже приняты) + +Предыдущий архитектор (Опус) принял 10 решений. Их нужно УЧЕСТЬ, но можно +ОСПОРИТЬ если найдёшь лучший подход. + +| # | Решение | Обоснование | +|---|---------|-------------| +| 1 | Отдельный процесс :5000 | `http_client` делает реальные HTTP-запросы — blueprint не проверит | +| 2 | Data-driven: сервисы из YAML | Не хардкодить 37 сервисов в Python | +| 3 | Мин. поля + `default_for(dataType)` | 20+ параметров вручную — ад | +| 4 | Ленивый dtFinish | Без потоков, `MOCK_OP_DELAY` (по умолчанию 0.1с) | +| 5 | Единая стейт-машина | create→running→suspended→deleted | +| 6 | Реальный мерж params при modify | Иначе тест modify→проверить state.params бессмысленен | +| 7 | Статический stateOut из YAML | Для MVP, генерация из параметров — потом | +| 8 | `/_mock/reset` для тестов | Без сброса тесты влияют друг на друга | +| 9 | Тесты через `app_client` | Проверяет реальную связку app-autotest ↔ эмулятор | +| 10 | refSvcId игнорируем в MVP | validate-cfs всегда OK, ссылки не проверяются | + +## 4. Критические точки интеграции (из кода app-autotest) + +Это НЕ предположения — это факты из реального кода, который будет ходить в polygon: + +### 4.1 Location-заголовки обязательны + +`HttpClient.post()` достаёт UUID из заголовка `Location` (последний сегмент, `len >= 32`). +Polygon ОБЯЗАН отдавать: +- `POST /api/v1/svc/instances` → `201` + `Location: ./{instanceUid}` +- `POST /api/v1/svc/instanceOperations` → `201` + `Location: ./{instanceOperationUid}` + +Без Location executor не получит UUID → ошибка. + +### 4.2 Поллинг + +`poll_until_done()` делает `GET /instanceOperations/{uid}?fields=...`, +ждёт `dtFinish`, спит 5 секунд между попытками. При `MOCK_OP_DELAY=0.1` +dtFinish появится на первом же GET — вторая итерация со сном не случится. + +### 4.3 Short-circuit localhost в auth.py + +`detect_endpoint()` в app-autotest хардкодит dev/test стенды и игнорирует +`NUBES_API_ENDPOINT`. Нужно добавить проверку: если endpoint начинается с +`http://localhost` или `http://127.0.0.1` → пропустить detect, сразу использовать. +Это будет сделано в app-autotest, не в polygon. + +### 4.4 validate-cfs = пустое тело + +`send_params_terraform()` считает успехом пустой/не-JSON ответ. +Polygon отдаёт `200` с пустым телом. + +### 4.5 state.params по коду, cfsParams по числовому id + +`get_params_with_current_values()` мержит `state.params[код]` с шаблоном из +`GET /instanceOperations/default/{opId}`. +YAML должен связывать числовой `svcOperationCfsParamId` ↔ код параметра. + +### 4.6 Nubes-совместимость + +Polygon деплоится как managed-сервис Nubes (pythonk8s). Следовательно: +- `site/app.py` — точка входа (платформа ждёт `python site/app.py`) +- `app.run(host="0.0.0.0", port=5000)` — обязательно +- ⛔ `site/__init__.py` — НЕЛЬЗЯ (конфликтует со stdlib `site.py`) +- ⛔ `from site.xxx import ...` — НЕЛЬЗЯ (импорт без префикса) +- `/health` → `"OK"` — healthcheck для Nubes + +## 5. Что нужно от тебя + +### 5.1 Архитектура проекта + +Полная архитектура polygon с обоснованием каждого решения. Включая: + +- **Структура файлов** в `site/` — какие модули, за что отвечают +- **Схема данных в памяти** — MockState: instances, operations, связи +- **Стейт-машина инстанса** — все статусы, переходы +- **Поток запроса** — что происходит от POST /instances до GET /instanceOperations/{uid} +- **Схема API** — полный список эндпоинтов с форматами запросов/ответов + +### 5.2 Универсальный конвертер STANDS → polygon + +Главный вопрос: **можно ли написать ОДИН конвертер для всех 37 сервисов без сервис-специфичного кода?** + +Если да — спроектировать `from_stands.py`: +- Алгоритм конвертации одного YAML +- Маппинг ВСЕХ полей STANDS → polygon (таблица) +- Обработка краевых случаев (map-fixed, sub_params, subresources) +- Генерация stateParams из create-операции (defaults) +- Генерация stateOut (из операций create_user/create_database и т.п.) +- Выходной формат: отдельные файлы в `services/` или один dict? + +Если нет — объяснить ГДЕ проходит граница и что придётся делать вручную. + +### 5.3 Подробный план реализации + +Пошаговый план с разбивкой на этапы. Для каждого этапа: +- Какие файлы создать/изменить +- Что именно реализовать (функции, классы, эндпоинты) +- Критерий готовности этапа +- Ожидаемые сложности + +## 6. Конкретные вопросы + +Ответь на каждый: + +### Q1. from_stands.py +Как обрабатывать `map-fixed` параметры (clusterConfiguration, startupConfiguration)? +В STANDS: `params[].sub_params[]`. В реальном API: `dataDescriptor`. +Должен ли конвертер генерировать `dataDescriptor` из `sub_params`? Или моку +достаточно знать структуру чтобы принимать такие параметры при create? + +### Q2. subresources +У postgres/mariadb есть `create_user`, `delete_user`, `create_database`, `delete_database`. +Можно ли вывести **общее правило**: «если операция имеет kind ≠ instance — это subresource»? +Или «если действие create/delete а операция не create/delete инстанса — subresource»? + +Как универсально эмулировать subresource-операции? Сейчас идея: `apply_effect` смотрит на +`action` и `kind` — если `kind != "instance"`, значит меняем `state.out`, не `state.params`. + +### Q3. stateOut +В реальном API после create PostgreSQL: `state.out = {users: {pgadmin: {}}, databases: {mydb: {}}}`. +Может ли конвертер автоматически вывести структуру stateOut из наличия subresource-операций? +Например: есть `create_user` → добавить `users: {}` в stateOut. + +### Q4. Лишние операции +У postgres есть restart, recovery. У некоторых сервисов есть специфичные операции. +Включать их в мок? Если да — как эмулировать? Если нет — что отвечать на запрос +`GET /instanceOperations/default/{id}` для несуществующей операции? + +### Q5. valueList +В STANDS YAML `value_list` есть у многих параметров. Нужно ли моку хранить их +и отдавать в `GET /instanceOperations/default/{id}` (cfsParams)? Или мок может +отдавать пустой `valueList` для всех? + +### Q6. Config loader vs генерация +Два подхода: +- **A)** `from_stands.py` генерит YAML в `services/`, `config_loader.py` их читает при старте +- **B)** `from_stands.py` сразу возвращает dict, без промежуточных YAML + +Какой лучше? Плюсы A: можно редактировать сгенерированные YAML руками для сложных сервисов. +Плюсы B: проще, меньше файлов. + +### Q7. mock-специфичные эндпоинты +`/_mock/reset` — сброс состояния. Нужны ли ещё? +- `/_mock/state` — посмотреть текущее состояние (для отладки)? +- `/_mock/delay/{seconds}` — изменить MOCK_OP_DELAY на лету? +- `/_mock/services` — список загруженных сервисов? + +### Q8. Идентификаторы +UUID для instanceUid и instanceOperationUid — генерить через `uuid.uuid4()`? +Или детерминированно (например, `uuid5(namespace, service_id + counter)`)? + +### Q9. Пагинация +`GET /instances?pageSize=N&page=P` — как правильно эмулировать? +Стоп по `len(batch) < pageSize`? Что если pageSize > 200? + +### Q10. Тестирование +5 сценариев из плана Опуса — достаточны? Какие ещё сценарии добавить? +Как организовать тесты чтобы они не зависели друг от друга? + +## 7. Что можно спрашивать у меня + +Ты можешь задавать мне уточняющие вопросы в текстовом виде. Я отвечу. + +Что можно спрашивать: +- Уточнения по STANDS YAML (структура конкретных сервисов, поля которые не видел) +- Детали работы app-autotest (какие запросы шлёт, какие форматы ждёт) +- Приоритеты (что важнее сделать сначала) +- Ограничения Nubes-платформы + +Что НЕ надо спрашивать: +- «Какие технологии использовать?» — только Flask, Python, PyYAML +- «Может использовать БД?» — нет, всё в памяти +- «Может Kubernetes API?» — нет, это чистый HTTP-эмулятор + +Формат вопросов: +``` +### Вопрос 1: <краткий заголовок> +<развёрнутый вопрос с контекстом> +``` diff --git a/polygon-docs/sonnet-response.md b/polygon-docs/sonnet-response.md new file mode 100644 index 0000000..cd5f778 --- /dev/null +++ b/polygon-docs/sonnet-response.md @@ -0,0 +1,229 @@ +# Ответ Claude Sonnet 4.6 — архитектура и план polygon + +Дата: 2026-07-31 + +--- + +## Ответы на Q1–Q10 + +**Q1: map-fixed → dataDescriptor** +Конвертер ГЕНЕРИРУЕТ `dataDescriptor` из `sub_params`. Формат каждой записи: +``` +{sub_param_code: {dataType, defaultValue, valueList, isRequired}} +``` +Это обязательно: `normalize_value` в app-autotest использует `dataDescriptor` для генерации JSON из defaults при пустом значении map-параметра. + +**Q2: subresources** +Правило простое и точное: `kind == "subresource"` (это явное поле в STANDS YAML). +`apply_effect` смотрит на `kind`: +- `kind == "instance"` → меняем статус + `state.params` +- `kind == "subresource"` → меняем `state.out[plural]`, статус НЕ меняем + +**Q3: stateOut auto-generation** +Да, конвертер выводит автоматически. Сканирует операции с `kind=subresource, action=create`, строит `state_out_template`. Пример для postgres: `{users: {}, databases: {}, backups: {}}`. При create инстанса мок копирует этот шаблон в `instance.state.out`. + +**Q4: "лишние" операции** +Включить все (restart, recovery, etc.) — иначе `GET /instanceOperations/default/{id}` вернёт 404 и app-autotest упадёт. Реализация: no-op transition (статус остаётся `running`, dtFinish ставится, isSuccessful=True). Если operation_id вообще не найден → 404 с `{"error": "operation not found"}`. + +**Q5: valueList** +Хранить и отдавать обязательно. `get_params_with_current_values` в app-autotest заполняет из него дропдауны для `delete_user`/`delete_database`. + +**Q6: Подход A (generate YAML → services/)** +Выбрать A. `from_stands.py` генерирует `site/services/N_name.yaml`, `config_loader.py` читает при старте Flask. Плюс: ручная правка для сложных случаев, прозрачность. `from_stands.py` запускается один раз при setup. + +**Q7: Mock-эндпоинты** +Все 4: +- `POST /_mock/reset` — обязателен для тестовой изоляции +- `GET /_mock/state` — текущее состояние (отладка) +- `GET /_mock/services` — список загруженных сервисов +- `POST /_mock/delay/` — изменить 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/` | Детали инстанса | +| 5 | POST | `/api/v1/svc/instances` | Создать shell → 201 + Location | +| 6 | GET | `/api/v1/svc/instanceOperations/default/` | Шаблон операции | +| 7 | POST | `/api/v1/svc/instanceOperations` | Создать операцию → 201 + Location | +| 8 | GET | `/api/v1/svc/instanceOperations/` | Детали операции + cfsParams | +| 9 | POST | `/api/v1/svc/instanceOperationCfsParams` | Задать значение параметра | +| 10 | GET | `/api/v1/svc/instanceOperations//validate-cfs` | Валидация → 200 **пустое тело** | +| 11 | POST | `/api/v1/svc/instanceOperations//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/` | Изменить 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/` должен стоять **выше** `/instanceOperations/` в 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()`. diff --git a/polygon-docs/tf-provider-refs.md b/polygon-docs/tf-provider-refs.md new file mode 100644 index 0000000..ce3e6cf --- /dev/null +++ b/polygon-docs/tf-provider-refs.md @@ -0,0 +1,103 @@ +# Ссылки: генератор YAML (tf_provider) + +Репозиторий Terraform-провайдера Nubes: `~/tf_provider` + +## Как генерируются STANDS YAML + +| Файл | Описание | +|------|----------| +| `~/tf_provider/README.md` | Общий пайплайн: 4 шага генерации провайдера | +| `~/tf_provider/docs/README.md` | **Главный индекс документации.** Формат токенов (не протухают), DDoS-Guard заголовки, Gateway URL | +| `~/tf_provider/docs/HOWTO_ADD_NEW_SERVICE.md` | Как добавить сервис: `services_list.txt` → `01_generate_yamls.sh` → API → YAML | +| `~/tf_provider/docs/ARCHITECTURE_NEW.md` | Архитектура: слои (core/provider/resources_gen/yaml), метод Виталия без instanceUid | +| `~/tf_provider/docs/70_api/api-discovery-algorithms.md` | **Ключевой документ.** Алгоритм получения параметров: `/instanceOperations/default/{id}` → `cfsParams` с `dataDescriptor`, `valueList`, `isModifiable` | + +## Критические API-эндпоинты (метод Виталия) + +Цепочка из 3 запросов — получение ВСЕХ параметров сервиса без создания инстанса: + +1. `GET /api/v1/svc/services` → найти `svcId` по имени +2. `GET /api/v1/svc/services/{svcId}` → найти `svcOperationId` операции (create: `{svc.operations[?operation=="create"].svcOperationId}`) +3. `GET /api/v1/svc/instanceOperations/default/{svcOperationId}` → **все cfsParams**: `dataDescriptor` (подполя map-fixed), `valueList`, `isModifiable`, `isSensitive`, `regex`, ... + +### Почему `/instanceOperations/default/{id}` а не `/serviceOperation/{id}`: + +| Данные | `/serviceOperation/{id}` | `/instanceOperations/default/{id}` | +|--------|--------------------------|-------------------------------------| +| ID, code, тип, isRequired | ✅ | ✅ | +| `valueList` (допустимые значения) | ❌ | ✅ | +| `dataDescriptor` (подполя map-fixed) | ❌ | ✅ | +| `isModifiable` | ❌ | ✅ | +| `regex`, `maxLength`, `minValue`... | ❌ | ✅ | +| `isSensitive` | ❌ | ✅ | + +### Формат valueList: + +- Для **верхнеуровневых** параметров: JSON-массив `["false", "true"]` +- Для **подполей** dataDescriptor: comma-separated строка `"1,3,5,7"` + +## Где взять реальные ответы API (для сверки мока) + +```bash +# Токены (НЕ протухают до декабря 2026): +~/tf_provider/secrets/test.token +~/tf_provider/secrets/dev.token +~/tf_provider/secrets/prod.token + +# Проверить дату токена: +python3 -c " +import json,base64 +t=open('$HOME/tf_provider/secrets/test.token').read().split('.') +d=json.loads(base64.urlsafe_b64decode(t[1]+'==')) +from datetime import datetime,timezone +print(datetime.fromtimestamp(d['exp'],tz=timezone.utc)) +" + +# Пример curl (test стенд): +curl -s --max-time 10 \ + -H "Authorization: Bearer $(cat ~/tf_provider/secrets/test.token)" \ + -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \ + -H "Referer: https://deck-test.ngcloud.ru/" \ + "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc/instanceOperations/default/" +``` + +### ⛔ 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} +```