doc: README для DOCS/HISTORY, polygon-docs/, +.venv в gitignore
This commit is contained in:
@@ -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: <краткий заголовок>
|
||||
<развёрнутый вопрос с контекстом>
|
||||
```
|
||||
Reference in New Issue
Block a user