doc: README для DOCS/HISTORY, polygon-docs/, +.venv в gitignore

This commit is contained in:
2026-07-31 20:39:48 +04:00
parent 633537a775
commit 6978d13d0e
7 changed files with 850 additions and 0 deletions
+2
View File
@@ -8,6 +8,7 @@ secrets/*
# Submodules / nested repos # Submodules / nested repos
app-autotest/ app-autotest/
polygon/
# OS files # OS files
.DS_Store .DS_Store
@@ -19,6 +20,7 @@ Thumbs.db
# IDE # IDE
.idea/ .idea/
.vscode/ .vscode/
.venv/
*.iml *.iml
# Go (embed.go generates from yaml) # Go (embed.go generates from yaml)
+103
View File
@@ -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 (тестовый стенд) |
+47
View File
@@ -504,3 +504,50 @@ convert_all(stands_dir) → dict[int, config]
Конвертер помечает такие операции: `{svcOperationId, operation, subresource, action}`. Конвертер помечает такие операции: `{svcOperationId, operation, subresource, action}`.
Один универсальный `apply_effect`, без сервис-специфичного кода. Один универсальный `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.`
+27
View File
@@ -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 |
## Формат
Каждая сессия содержит:
- Контекст (что обсуждалось)
- Принятые решения
- Реализованные изменения (версии)
- Ошибки и как исправлены
+339
View File
@@ -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: <краткий заголовок>
<развёрнутый вопрос с контекстом>
```
+229
View File
@@ -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/<float>` — изменить MOCK_OP_DELAY на лету
**Q8: UUID**`uuid.uuid4()`. Детерминированные не нужны.
**Q9: Пагинация** → stop when `len(batch) < pageSize`, cap at 200, page=1 by default. Response: `{"results": [...], "pageSize": N, "page": P, "total": len(all)}`.
**Q10: Тесты**
К 5 сценариям Опуса добавить: (a) тест пагинации (3 инстанса, pageSize=2), (b) тест subresource create→verify state.out→delete→verify removed, (c) тест modify→verify params merged, (d) тест /_mock/state, (e) тест 404 на невалидный op_id. Каждый тест начинается с `POST /_mock/reset` (через `autouse` fixture).
---
## Структура файлов
```
polygon/
├── requirements.txt
└── site/
├── app.py # Flask app + все 15 маршрутов (один файл)
├── mock_state.py # MockState: instances, operations, op_params
├── state_machine.py # apply_effect()
├── config_loader.py # load_services() → services_dict + ops_index
├── from_stands.py # CLI-конвертер STANDS YAML → services/*.yaml
├── services/ # 37 YAML-файлов (generated)
└── templates/
└── index.html
```
**Ключевое ограничение Nubes:**
- `site/__init__.py` — ЗАПРЕЩЁН
- `from site.xxx import` — ЗАПРЕЩЁН
- В app.py: только `import mock_state`, `import config_loader`, `import state_machine` (работает т.к. Python добавляет директорию скрипта в `sys.path[0]`)
---
## Схема данных в памяти
```python
# mock_state.py — синглтон state = MockState()
class MockState:
instances = {} # instanceUid → instance_dict
operations = {} # opUid → operation_dict
op_params = {} # opUid → {paramId(int): paramValue(str)}
```
**instance_dict:** `{instanceUid, serviceId, displayName, descr, status, explainedStatus, svc, dtCreate, state: {params: {code: val}, out: {users: {}, databases: {}}}}`
**operation_dict:** `{instanceOperationUid, instanceUid, svcOperationId, operation, kind, action, subresource|None, dtStart, dtFinish|None, isSuccessful|None, errorLog, svc, stages}`
---
## Конвертер from_stands.py
**HTML entities:** STANDS YAML содержит `integer &gt;= 0`, `&quot;` и т.д. `from_stands.py` **обязан** применять `html.unescape()` ко всем строковым полям (`data_type`, `value_list` элементы, `default`).
**Маппинг STANDS → cfsParam формат:**
| STANDS | polygon/cfsParam |
|---|---|
| `id` | `svcOperationCfsParamId` |
| `code` | `svcOperationCfsParam` |
| `data_type` (unescape) | `dataType` |
| `required` | `isRequired` |
| `default` | `defaultValue` |
| `value_list` | `valueList` |
| `sub_params` (map-fixed) | → `dataDescriptor: {code: {dataType,defaultValue,valueList,isRequired}}` |
| `sub_params` (map) | → `sub_params` (хранить как есть для normalize_value) |
**stateOut auto-generation:**
```python
for op in operations:
if op.kind == "subresource" and op.action == "create":
plural = op.subresource + "s" # user→users, database→databases
state_out_template[plural] = {}
```
---
## Полный список API эндпоинтов (15 шт.)
| # | Метод | Путь | Назначение |
|---|---|---|---|
| 1 | GET | `/health` | `"OK"` |
| 2 | GET | `/` | HTML-страница |
| 3 | GET | `/api/v1/svc/instances` | Список инстансов (paged) |
| 4 | GET | `/api/v1/svc/instances/<uid>` | Детали инстанса |
| 5 | POST | `/api/v1/svc/instances` | Создать shell → 201 + Location |
| 6 | GET | `/api/v1/svc/instanceOperations/default/<int:op_id>` | Шаблон операции |
| 7 | POST | `/api/v1/svc/instanceOperations` | Создать операцию → 201 + Location |
| 8 | GET | `/api/v1/svc/instanceOperations/<uid>` | Детали операции + cfsParams |
| 9 | POST | `/api/v1/svc/instanceOperationCfsParams` | Задать значение параметра |
| 10 | GET | `/api/v1/svc/instanceOperations/<uid>/validate-cfs` | Валидация → 200 **пустое тело** |
| 11 | POST | `/api/v1/svc/instanceOperations/<uid>/run` | Выполнить операцию |
| 12 | POST | `/api/v1/svc/_mock/reset` | Сброс состояния |
| 13 | GET | `/api/v1/svc/_mock/state` | Debug: текущее состояние |
| 14 | GET | `/api/v1/svc/_mock/services` | Debug: загруженные сервисы |
| 15 | POST | `/api/v1/svc/_mock/delay/<float:s>` | Изменить MOCK_OP_DELAY |
**Критические детали:**
- **Эндпоинт 5** (`POST /instances`): возвращает `201` + заголовок `Location: ./{uid}`. `HttpClient.post()` берёт uid из последнего сегмента Location, проверяет `len >= 32`. Body: `{"instanceUid": uid}` (двойная защита).
- **Эндпоинт 7** (`POST /instanceOperations`): при `operation=="create"` тело НЕ содержит `svcOperationId` — polygon находит его сам из `svc_def.operations["create"]["id"]`.
- **Эндпоинт 10** (validate-cfs): `return "", 200` (НЕ `jsonify`). app-autotest ловит `JSONDecodeError` и считает это успехом.
- **Эндпоинт 11** (run): синхронный — `time.sleep(MOCK_OP_DELAY)`, затем `apply_effect`, затем `dtFinish = datetime.utcnow().isoformat() + "Z"`. Первый poll после run увидит dtFinish.
- **Маршруты Flask:** `/instanceOperations/default/<int:op_id>` должен стоять **выше** `/instanceOperations/<uid>` в app.py.
---
## Поток запроса (полный цикл create)
```
executor.py polygon
───────── ───────
POST /instances {serviceId,displayName}
→ создать instance shell (status="creating")
→ response 201 + Location: ./uid1
← instanceUid = uid1
POST /instanceOperations {instanceUid, operation:"create"}
→ найти create op_id из service_def
→ создать operation_dict (dtFinish=None)
→ response 201 + Location: ./uid2
← opUid = uid2
GET /instanceOperations/uid2?fields=cfsParams
→ response {"instanceOperation": {"cfsParams": [...]}}
cfsParams из service_def + пустые paramValue
POST /instanceOperationCfsParams × N → state.op_params[uid2][paramId] = val
GET /instanceOperations/uid2/validate-cfs → response "" 200
POST /instanceOperations/uid2/run
→ sleep(0.1s)
→ apply_effect: state.params←codes, status="running"
→ dtFinish = now
→ response {"ok": true}
GET /instanceOperations/uid2?fields=dtFinish,...
→ response {"instanceOperation": {"dtFinish": "2026-...", "isSuccessful": true, ...}}
← poll done: status="OK"
```
---
## Фазы реализации
**Этап 1 — Конвертер** (`from_stands.py`): html.unescape, convert_param рекурсивный, build_state_out_template, pluralize, CLI. Запустить: `cd polygon/site && python from_stands.py`. Критерий: 37 YAML в `site/services/`, у postgres `state_out_template: {users:{}, databases:{}, backups:{}}`.
**Этап 2 — MockState + config_loader + state_machine**: три отдельных модуля. Критерий: `python -c "import config_loader; s,i=config_loader.load_services('services'); print(len(s))"` → 37.
**Этап 3 — Flask API** в app.py: реализовать все 15 эндпоинтов в порядке от простых к сложным (см. таблицу). Критерий: `python -m py_compile app.py` без ошибок, `curl localhost:5000/health` → OK.
**Этап 4 — Тесты**:
- В polygon: `tests/test_converter.py`, `tests/test_state_machine.py` (юнит)
- В app-autotest: `tests/conftest.py` (+fixture `polygon_server` subprocess), `tests/test_polygon_integration.py` (интеграционные)
## Уточнения (второй раунд)
### Уточнение 1: `map` с `sub_params` → тоже `dataDescriptor`
Правило: **если у параметра есть `sub_params` — генерировать `dataDescriptor`**, независимо от того `map` это или `map-fixed`.
| `data_type` | `sub_params` | Действие |
|---|---|---|
| `map-fixed` | есть | генерировать `dataDescriptor` |
| `map` | есть | генерировать `dataDescriptor` |
| `array-map-fixed` | есть | генерировать `dataDescriptor` |
| любой | нет | `dataDescriptor: null` |
### Уточнение 2: `state.out` после create — пустые `{}`
Достаточно пустых `{}` из `state_out_template`. STANDS YAML не описывает runtime-дефолты (`pgadmin`, `mydb`) — это эффект реального Terraform, не мок.
### Уточнение 3: расположение тестов
```
app-autotest/
tests/
conftest.py # + fixture polygon_server (subprocess)
test_polygon_integration.py # интеграционные тесты
polygon/
tests/
test_converter.py # юнит: from_stands.py
test_state_machine.py # юнит: apply_effect
```
`polygon_server` fixture: subprocess → poll `/health` (timeout 5s) → после тестов `terminate()`.
+103
View File
@@ -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/<opId>"
```
### ⛔ DDoS-Guard: ВСЕГДА нужны 3 заголовка
**Без них — 403 Forbidden даже с валидным токеном.**
```bash
-H "Authorization: Bearer $TOKEN"
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
-H "Referer: https://deck-{stand}.ngcloud.ru/"
```
### URL стендов (Gateway)
| Стенд | Gateway URL | Referer |
|-------|-------------|---------|
| test | `https://lk-api-gateway-test.ngcloud.ru/api/v1/svc` | `https://deck-test.ngcloud.ru/` |
| dev | `https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc` | `https://deck-dev.ngcloud.ru/` |
| prod | `https://lk-api-gateway.ngcloud.ru/api/v1/svc` | `https://deck.ngcloud.ru/` |
## Сгенерированные YAML (все стенды)
| Стенд | Сервисов | Путь |
|-------|----------|------|
| dev | 50 | `~/tf_provider/generated/dev/resources_yaml/` |
| test | 48 | `~/tf_provider/generated/test/resources_yaml/` |
| prod | 46 | `~/tf_provider/generated/prod/resources_yaml/` |
44 сервиса идентичны на всех трёх стендах.
## Поток данных: API → polygon
```
API Nubes (реальный)
→ 01_generate_yamls.sh
→ ~/tf_provider/generated/test/resources_yaml/*.yaml
→ копируются в STANDS/test/resources_yaml/ (autotest)
→ from_stands.py конвертирует
→ polygon/site/services/*.yaml
→ config_loader.py загружает при старте
→ polygon эмулирует /instanceOperations/default/{id}
```